diff --git a/.buildkite/beta-builds.yml b/.buildkite/beta-builds.yml index 9d77c5e399b9..d54c253621cb 100644 --- a/.buildkite/beta-builds.yml +++ b/.buildkite/beta-builds.yml @@ -5,7 +5,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/a8c-ci-toolkit#2.17.0 + - automattic/a8c-ci-toolkit#3.4.2 steps: ################# diff --git a/.buildkite/code-freeze.yml b/.buildkite/code-freeze.yml index 2f63df2ba25b..d46b6568e8a9 100644 --- a/.buildkite/code-freeze.yml +++ b/.buildkite/code-freeze.yml @@ -2,7 +2,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/a8c-ci-toolkit#2.17.0 + - automattic/a8c-ci-toolkit#3.4.2 steps: - label: "Code Freeze" diff --git a/.buildkite/complete-code-freeze.yml b/.buildkite/complete-code-freeze.yml index 5f1b22ee87b2..c50c7ff05254 100644 --- a/.buildkite/complete-code-freeze.yml +++ b/.buildkite/complete-code-freeze.yml @@ -2,7 +2,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/a8c-ci-toolkit#2.17.0 + - automattic/a8c-ci-toolkit#3.4.2 steps: - label: "Complete Code Freeze" diff --git a/.buildkite/finalize-release.yml b/.buildkite/finalize-release.yml index 988b6f9e6905..7c03f7137228 100644 --- a/.buildkite/finalize-release.yml +++ b/.buildkite/finalize-release.yml @@ -2,7 +2,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/a8c-ci-toolkit#2.17.0 + - automattic/a8c-ci-toolkit#3.4.2 steps: - label: "Finalize release" diff --git a/.buildkite/new-beta-release.yml b/.buildkite/new-beta-release.yml index 3ce0d1503059..0cde24c0912b 100644 --- a/.buildkite/new-beta-release.yml +++ b/.buildkite/new-beta-release.yml @@ -2,7 +2,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/a8c-ci-toolkit#2.17.0 + - automattic/a8c-ci-toolkit#3.4.2 steps: - label: "New Beta Release" diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index c6c5356e0c4d..290f05477e29 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -2,7 +2,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &ci_toolkit - automattic/a8c-ci-toolkit#2.18.2 + automattic/a8c-ci-toolkit#3.4.2 - &test_collector test-collector#v1.8.0 - &test_collector_common_params diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml index c15541c25f66..0e44afed36d1 100644 --- a/.buildkite/release-builds.yml +++ b/.buildkite/release-builds.yml @@ -5,7 +5,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/a8c-ci-toolkit#2.17.0 + - automattic/a8c-ci-toolkit#3.4.2 steps: ################# diff --git a/.buildkite/update-release-notes.yml b/.buildkite/update-release-notes.yml index 67748b4711aa..dcdca7b56518 100644 --- a/.buildkite/update-release-notes.yml +++ b/.buildkite/update-release-notes.yml @@ -2,7 +2,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/a8c-ci-toolkit#2.17.0 + - automattic/a8c-ci-toolkit#3.4.2 steps: - label: "Update release notes" diff --git a/.github/workflows/run-danger.yml b/.github/workflows/run-danger.yml index 1031d64a7dd9..ec845c47c73a 100644 --- a/.github/workflows/run-danger.yml +++ b/.github/workflows/run-danger.yml @@ -7,7 +7,7 @@ on: jobs: dangermattic: if: ${{ (github.event.pull_request.draft == false) }} - uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.0 + uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.2 with: org-slug: "automattic" pipeline-slug: "wordpress-android" diff --git a/.github/workflows/validate-issues.yml b/.github/workflows/validate-issues.yml index c9899d62603d..01b05ac2ffe2 100644 --- a/.github/workflows/validate-issues.yml +++ b/.github/workflows/validate-issues.yml @@ -6,7 +6,7 @@ on: jobs: check-labels-on-issues: - uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.0.0 + uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.1.2 with: label-format-list: '[ "^\[.+\]", diff --git a/Gemfile.lock b/Gemfile.lock index 631493a457ee..5222314be129 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,14 +69,14 @@ GEM no_proxy_fix octokit (>= 4.0) terminal-table (>= 1, < 4) - danger-dangermattic (1.0.2) + danger-dangermattic (1.1.1) danger (~> 9.4) danger-plugin-api (~> 1.0) - danger-rubocop (~> 0.12) - rubocop (~> 1.61) + danger-rubocop (~> 0.13) + rubocop (~> 1.63) danger-plugin-api (1.0.0) danger (> 2.0) - danger-rubocop (0.12.0) + danger-rubocop (0.13.0) danger rubocop (~> 1.0) declarative (0.0.20) @@ -259,7 +259,7 @@ GEM optparse (0.4.0) os (1.1.4) parallel (1.24.0) - parser (3.3.1.0) + parser (3.3.2.0) ast (~> 2.4.1) racc plist (3.7.1) @@ -267,22 +267,23 @@ GEM highline (>= 1.6, < 3) options (~> 2.3.0) public_suffix (5.0.5) - racc (1.7.3) + racc (1.8.0) rainbow (3.1.1) rake (13.2.1) rake-compiler (1.2.7) rake rchardet (1.8.0) - regexp_parser (2.9.1) + regexp_parser (2.9.2) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.2.8) + strscan (>= 3.0.9) rmagick (4.3.0) rouge (2.0.7) - rubocop (1.63.5) + rubocop (1.64.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -310,6 +311,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/README.md b/README.md index 6de18b6906d5..abdabaee6db2 100644 --- a/README.md +++ b/README.md @@ -16,41 +16,8 @@ If you're a developer wanting to contribute, read on. Notes: -* To use WordPress.com features (login to WordPress.com, access Reader and Stats, etc) you need a WordPress.com OAuth2 ID and secret. Please read the [OAuth2 Authentication](#oauth2-authentication) section. * While loading/building the app in Android Studio ignore the prompt to update the gradle plugin version as that will probably introduce build errors. On the other hand, feel free to update if you are planning to work on ensuring the compatibility of the newer version. - -## 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 "**Website URL**", "**Redirect URLs**", and "**Javascript Origins**" fields are required but not used for -the mobile apps. Just use "**[https://localhost](https://localhost)**". - -Once you've created your application in the [applications manager][5], you'll -need to edit the `./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. Note that authenticating to WordPress.com via Google is not supported -in development builds of the app, only in the official release. - -Note that credentials created with our [WordPress.com applications manager][5] -allow login only and not signup. New accounts must be created using the [official app][1] -or [on the web](https://wordpress.com/start). Login is restricted to the WordPress.com -account with which the credentials were created. In other words, if the credentials -were created with foo@email.com, you will only be able to login with foo@email.com. -Using another account like bar@email.com will cause the `Client cannot use "password" grant_type` error. - -For security reasons, some account-related actions aren't supported for development -builds when using a WordPress.com account with 2-factor authentication enabled. - -Read more about [OAuth2][6] and the [WordPress.com REST endpoint][7]. - ## Build and Test ## To build, install, and test the project from the command line: @@ -61,6 +28,13 @@ To build, install, and test the project from the command line: $ ./gradlew :WordPress:testWordPressVanillaDebugUnitTest # assemble, install and run unit tests $ ./gradlew :WordPress:connectedWordPressVanillaDebugAndroidTest # assemble, install and run Android tests +## Running the app ## + +You can use your own WordPress site for developing and testing the app. If you don't have one, you can create a temporary test site for free at https://jurassic.ninja/. +On the app start up screen, choose "Enter your existing site address" and enter the URL of your site and your credentials. + +Note: Access to WordPress.com features is temporarily disabled in the development environment. + ## Directory structure ## . ├── libs # dependencies used to build debug variants diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskHandler.kt b/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskHandler.kt new file mode 100644 index 000000000000..b1bdfecde5a8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskHandler.kt @@ -0,0 +1,33 @@ +package org.wordpress.android.datasets + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Helper class to handle async tasks by using coroutines + * @see Introduction + */ +object AsyncTaskHandler { + /** + * Load data in the background and handle the result on the main thread + */ + @JvmStatic + fun load(backgroundTask: () -> T, callback: AsyncTaskCallback) { + CoroutineScope(Dispatchers.IO).launch { + // handle the background task + val result = backgroundTask() + + withContext(Dispatchers.Main) { + // handle the result on the main thread + callback.onTaskFinished(result) + } + } + } + + interface AsyncTaskCallback { + fun onTaskFinished(result: T) + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteViewHolder.kt index 3d72d0476dee..9e169ba7f29f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteViewHolder.kt @@ -88,10 +88,7 @@ class ChooseSiteViewHolder(private val binding: ItemChooseSiteBinding) : Recycle } private fun handleAvatar(site: SiteRecord) { - imageManager.loadImageWithCorners( - binding.avatar, site.blavatarType, site.blavatarUrl, - itemView.context.resources.getDimensionPixelSize(R.dimen.blavatar_sz) / 2 - ) + imageManager.load(binding.avatar, site.blavatarType, site.blavatarUrl) val isDarkTheme = itemView.resources.configuration.isDarkTheme() val borderColor = ContextCompat.getColor( itemView.context, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadReadyListener.java b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadReadyListener.java index b8ebe1d28fc1..fafcc37c834f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadReadyListener.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadReadyListener.java @@ -1,5 +1,6 @@ package org.wordpress.android.ui.media.services; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.wordpress.android.fluxc.model.PostModel; @@ -14,7 +15,7 @@ public interface MediaUploadReadyListener { // TODO: We're passing a SiteModel parameter here in order to debug a crash on SaveStoryGutenbergBlockUseCase. // Once that's done, the parameter should be replaced with a site url String, like it was before. // See: https://git.io/JqfhK - PostModel replaceMediaFileWithUrlInPost(@Nullable PostModel post, String localMediaId, MediaFile mediaFile, + PostModel replaceMediaFileWithUrlInPost(@Nullable PostModel post, @NonNull String localMediaId, MediaFile mediaFile, @Nullable SiteModel site); PostModel markMediaUploadFailedInPost(@Nullable PostModel post, String localMediaId, MediaFile mediaFile); } 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 index ae3c3b0fcbf4..8537f9d70275 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java @@ -430,7 +430,8 @@ public static boolean shouldShowGutenbergEditor(boolean isNewPost, String postCo } public static String replaceMediaFileWithUrlInGutenbergPost(@NonNull String postContent, - String localMediaId, MediaFile mediaFile, String siteUrl) { + @NonNull String localMediaId, MediaFile mediaFile, + String siteUrl) { if (mediaFile != null && contentContainsGutenbergBlocks(postContent)) { MediaUploadCompletionProcessor processor = new MediaUploadCompletionProcessor(localMediaId, mediaFile, siteUrl); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/AudioBlockProcessor.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/AudioBlockProcessor.kt index 8389eb1e4be7..03e2ac4a1886 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/AudioBlockProcessor.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/AudioBlockProcessor.kt @@ -4,14 +4,14 @@ import com.google.gson.JsonObject import org.jsoup.nodes.Document import org.wordpress.android.util.helpers.MediaFile -class AudioBlockProcessor(localId: String?, mediaFile: MediaFile?) : BlockProcessor(localId, mediaFile) { +class AudioBlockProcessor(localId: String, mediaFile: MediaFile) : BlockProcessor(localId, mediaFile) { override fun processBlockContentDocument(document: Document?): Boolean { val audioElements = document?.select(AUDIO_TAG) audioElements?.let { elements -> for (element in elements) { // replaces the src attribute's local url with the remote counterpart. - element.attr(SRC_ATTRIBUTE, mRemoteUrl) + element.attr(SRC_ATTRIBUTE, remoteUrl) } return true } @@ -21,9 +21,9 @@ class AudioBlockProcessor(localId: String?, mediaFile: MediaFile?) : BlockProces override fun processBlockJsonAttributes(jsonAttributes: JsonObject?): Boolean { val id = jsonAttributes?.get(ID_ATTRIBUTE) - return if (id != null && !id.isJsonNull && id.asString == mLocalId) { + return if (id != null && !id.isJsonNull && id.asString == localId) { jsonAttributes.apply { - addIntPropertySafely(this, ID_ATTRIBUTE, mRemoteId) + addIntPropertySafely(this, ID_ATTRIBUTE, remoteId) } true } else { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java deleted file mode 100644 index 70ac05ac116c..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java +++ /dev/null @@ -1,182 +0,0 @@ -package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Document.OutputSettings; -import org.wordpress.android.editor.Utils; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.helpers.MediaFile; - -import java.util.regex.Matcher; - -import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_BLOCK_CAPTURES; -import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_SELF_CLOSING_BLOCK_CAPTURES; -import static org.wordpress.android.util.AppLog.T.MEDIA; - -/** - * Abstract class to be extended for each enumerated {@link MediaBlockType}. - */ -public abstract class BlockProcessor { - /** - * HTML output used by the parser - */ - @SuppressWarnings("checkstyle:LineLength") static final OutputSettings OUTPUT_SETTINGS = new OutputSettings() - .outline(false) -// .syntax(Syntax.xml) -// Do we want xml or html here (e.g. self closing tags, boolean attributes)? -// https://stackoverflow.com/questions/26584974/keeping-html-boolean-attributes-in-their-original-form-when-parsing-with-jsoup - .prettyPrint(false); - - String mLocalId; - String mRemoteId; - String mRemoteUrl; - String mRemoteGuid; - - private String mBlockName; - private JsonObject mJsonAttributes; - private Document mBlockContentDocument; - private String mClosingComment; - - - /** - * @param localId The local media id that needs replacement - * @param mediaFile The mediaFile containing the remote id and remote url - */ - BlockProcessor(String localId, MediaFile mediaFile) { - mLocalId = localId; - mRemoteId = mediaFile.getMediaId(); - mRemoteUrl = org.wordpress.android.util.StringUtils.notNullStr(Utils.escapeQuotes(mediaFile - .getOptimalFileURL())); - mRemoteGuid = mediaFile.getVideoPressGuid(); - } - - private JsonObject parseJson(String blockJson) { - JsonParser parser = new JsonParser(); - return parser.parse(blockJson).getAsJsonObject(); - } - - private Document parseHTML(String blockContent) { - // create document from block content - Document document = Jsoup.parse(blockContent); - document.outputSettings(OUTPUT_SETTINGS); - return document; - } - - private boolean splitBlock(String block, Boolean isSelfClosingTag) { - Matcher captures = ( - isSelfClosingTag ? PATTERN_SELF_CLOSING_BLOCK_CAPTURES : PATTERN_BLOCK_CAPTURES - ).matcher(block); - - boolean capturesFound = captures.find(); - - if (capturesFound) { - mBlockName = captures.group(1); - mJsonAttributes = parseJson(captures.group(2)); - mBlockContentDocument = isSelfClosingTag ? null : parseHTML(captures.group(3)); - mClosingComment = isSelfClosingTag ? null : captures.group(4); - return true; - } else { - mBlockName = null; - mJsonAttributes = null; - mBlockContentDocument = null; - mClosingComment = null; - return false; - } - } - - /** - * Processes a block returning a raw content replacement string. If a match is not found for the block content, this - * method should return the original block contents unchanged. - * - * @param block The raw block contents - * @param isSelfClosingTag True if the block tag is self-closing (e.g. ) - * @return A string containing content with ids and urls replaced - */ - String processBlock(String block, Boolean isSelfClosingTag) { - if (splitBlock(block, isSelfClosingTag)) { - if (processBlockJsonAttributes(mJsonAttributes)) { - if (isSelfClosingTag) { - // return injected block - return new StringBuilder() - .append("") - .toString(); - } else if (processBlockContentDocument(mBlockContentDocument)) { - // return injected block - return new StringBuilder() - .append("\n") - .append(mBlockContentDocument.body().html()) // HTML parser output - .append(mClosingComment) - .toString(); - } - } else { - return processInnerBlock(block); // delegate to inner blocks if needed - } - } - // leave block unchanged - return block; - } - - String processBlock(String block) { - return processBlock(block, false); - } - - final void addIntPropertySafely(JsonObject jsonAttributes, String propertyName, String value) { - try { - jsonAttributes.addProperty(propertyName, Integer.parseInt(value)); - } catch (NumberFormatException e) { - AppLog.e(MEDIA, e.getMessage()); - } - } - - /** - * All concrete implementations must implement this method for the particular block type. The document represents - * the html contents of the block to be processed, and is to be mutated in place.
- *
- * This method should return true to indicate success. Returning false will result in the block contents being - * unmodified. - * - * @param document The document to be mutated to make the necessary replacements - * @return A boolean value indicating whether or not the block contents should be replaced - */ - abstract boolean processBlockContentDocument(Document document); - - /** - * All concrete implementations must implement this method for the particular block type. The jsonAttributes object - * is a {@link JsonObject} parsed from the block header attributes. This object can be used to check for a match, - * and can be directly mutated if necessary.
- *
- * This method should return true to indicate success. Returning false will result in the block contents being - * unmodified. - * - * @param jsonAttributes the attributes object used to check for a match with the local id, and mutated if necessary - * @return - */ - abstract boolean processBlockJsonAttributes(JsonObject jsonAttributes); - - /** - * This method can be optionally overriden by concrete implementations to delegate further processing via recursion - * when {@link BlockProcessor#processBlockJsonAttributes(JsonObject)} returns false (i.e. the block did not match - * the local id being replaced). This is useful for implementing mutual recursion with - * {@link MediaUploadCompletionProcessor#processContent(String)} for block types that have media-containing blocks - * within their inner content.
- *
- * The default implementation provided is a NOOP that leaves the content of the block unchanged. - * - * @param block The raw block contents - * @return A string containing content with ids and urls replaced - */ - String processInnerBlock(String block) { - return block; - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.kt new file mode 100644 index 000000000000..b024416d51f4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.kt @@ -0,0 +1,168 @@ +package org.wordpress.android.ui.posts.mediauploadcompletionprocessors + +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.wordpress.android.editor.Utils +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.StringUtils +import org.wordpress.android.util.helpers.MediaFile + +/** + * Abstract class to be extended for each enumerated [MediaBlockType]. + */ +abstract class BlockProcessor internal constructor(@JvmField var localId: String, mediaFile: MediaFile) { + @JvmField + var remoteId: String = mediaFile.mediaId + + @JvmField + var remoteUrl: String = StringUtils.notNullStr(Utils.escapeQuotes(mediaFile.optimalFileURL)) + var remoteGuid: String? = mediaFile.videoPressGuid + + private var blockName: String? = null + private var jsonAttributes: JsonObject? = null + private var blockContentDocument: Document? = null + private var closingComment: String? = null + + private fun parseJson(blockJson: String) = JsonParser.parseString(blockJson).asJsonObject + + private fun parseHTML(blockContent: String): Document { + // create document from block content + val document = Jsoup.parse(blockContent) + document.outputSettings(OUTPUT_SETTINGS) + return document + } + + private fun splitBlock(block: String, isSelfClosingTag: Boolean): Boolean { + val pattern = if (isSelfClosingTag) { + MediaUploadCompletionProcessorPatterns.PATTERN_SELF_CLOSING_BLOCK_CAPTURES + } else { + MediaUploadCompletionProcessorPatterns.PATTERN_BLOCK_CAPTURES + } + val captures = pattern.matcher(block) + + val capturesFound = captures.find() + + return if (capturesFound) { + blockName = captures.group(GROUP_BLOCK_NAME) + jsonAttributes = captures.group(GROUP_JSON_ATTRIBUTES)?.let { parseJson(it) } + blockContentDocument = if (isSelfClosingTag) { + null + } else { + captures.group(GROUP_BLOCK_CONTENT_DOCUMENT)?.let { parseHTML(it) } + } + closingComment = if (isSelfClosingTag) null else captures.group(GROUP_CLOSING_COMMENT) + true + } else { + blockName = null + jsonAttributes = null + blockContentDocument = null + closingComment = null + false + } + } + + /** + * Processes a block returning a raw content replacement string. If a match is not found for the block content, this + * method should return the original block contents unchanged. + * + * @param block The raw block contents + * @param isSelfClosingTag True if the block tag is self-closing (e.g. ) + * @return A string containing content with ids and urls replaced + */ + @JvmOverloads + fun processBlock(block: String, isSelfClosingTag: Boolean = false) = if (splitBlock(block, isSelfClosingTag)) { + if (processBlockJsonAttributes(jsonAttributes)) { + if (isSelfClosingTag) { + // return injected block + StringBuilder() + .append("") + .toString() + } else if (processBlockContentDocument(blockContentDocument)) { + // return injected block + StringBuilder() + .append("\n") + .append(blockContentDocument?.body()?.html()) // HTML parser output + .append(closingComment) + .toString() + } else { + block + } + } else { + processInnerBlock(block) // delegate to inner blocks if needed + } + } else { + // leave block unchanged + block + } + + fun addIntPropertySafely(jsonAttributes: JsonObject, propertyName: String, value: String) = try { + jsonAttributes.addProperty(propertyName, value.toInt()) + } catch (e: NumberFormatException) { + AppLog.e(AppLog.T.MEDIA, e.message) + } + + /** + * All concrete implementations must implement this method for the particular block type. The document represents + * the html contents of the block to be processed, and is to be mutated in place.

+ *

+ * This method should return true to indicate success. Returning false will result in the block contents being + * unmodified. + * + * @param document The document to be mutated to make the necessary replacements + * @return A boolean value indicating whether or not the block contents should be replaced + */ + abstract fun processBlockContentDocument(document: Document?): Boolean + + /** + * All concrete implementations must implement this method for the particular block type. The jsonAttributes object + * is a [JsonObject] parsed from the block header attributes. This object can be used to check for a match, and can + * be directly mutated if necessary.

+ *

+ * This method should return true to indicate success. Returning false will result in the block contents being + * unmodified. + * + * @param jsonAttributes the attributes object used to check for a match with the local id, and mutated if necessary + * @return + */ + abstract fun processBlockJsonAttributes(jsonAttributes: JsonObject?): Boolean + + /** + * This method can be optionally overridden by concrete implementations to delegate further processing via recursion + * when [BlockProcessor.processBlockJsonAttributes] returns false (i.e. the block did not match the local id being + * replaced). This is useful for implementing mutual recursion with + * [MediaUploadCompletionProcessor.processContent] for block types that have media-containing blocks within their + * inner content.

+ *

+ * The default implementation provided is a NOOP that leaves the content of the block unchanged. + * + * @param block The raw block contents + * @return A string containing content with ids and urls replaced + */ + open fun processInnerBlock(block: String) = block + + companion object { + /** + * HTML output used by the parser + */ + val OUTPUT_SETTINGS: Document.OutputSettings = Document.OutputSettings() + .outline(false) +// .syntax(Syntax.xml) +// Do we want xml or html here (e.g. self closing tags, boolean attributes)? +// https://stackoverflow.com/questions/26584974/keeping-html-boolean-attributes-in-their-original-form-when-parsing-with-jsoup + .prettyPrint(false) + private const val GROUP_BLOCK_NAME = 1 + private const val GROUP_JSON_ATTRIBUTES = 2 + private const val GROUP_BLOCK_CONTENT_DOCUMENT = 3 + private const val GROUP_CLOSING_COMMENT = 4 + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessorFactory.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessorFactory.java index 463bb428090c..1e8fd766a95c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessorFactory.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessorFactory.java @@ -1,5 +1,7 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import androidx.annotation.NonNull; + import org.wordpress.android.util.helpers.MediaFile; import java.util.HashMap; @@ -33,7 +35,7 @@ class BlockProcessorFactory { * @param siteUrl The site url - used to generate the attachmentPage url * @return The factory instance - useful for chaining this method upon instantiation */ - BlockProcessorFactory init(String localId, MediaFile mediaFile, String siteUrl) { + BlockProcessorFactory init(@NonNull String localId, @NonNull MediaFile mediaFile, String siteUrl) { mMediaBlockTypeBlockProcessorMap.put(IMAGE, new ImageBlockProcessor(localId, mediaFile)); mMediaBlockTypeBlockProcessorMap.put(VIDEOPRESS, new VideoPressBlockProcessor(localId, mediaFile)); mMediaBlockTypeBlockProcessorMap.put(VIDEO, new VideoBlockProcessor(localId, mediaFile)); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java index 71163e023b82..d9ab600428cd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java @@ -1,5 +1,8 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -29,13 +32,15 @@ public class CoverBlockProcessor extends BlockProcessor { private final MediaUploadCompletionProcessor mMediaUploadCompletionProcessor; - public CoverBlockProcessor(String localId, MediaFile mediaFile, + public CoverBlockProcessor(@NonNull String localId, @NonNull MediaFile mediaFile, MediaUploadCompletionProcessor mediaUploadCompletionProcessor) { super(localId, mediaFile); mMediaUploadCompletionProcessor = mediaUploadCompletionProcessor; } - @Override String processInnerBlock(String block) { + @NonNull + @Override + public String processInnerBlock(@NonNull String block) { Matcher innerMatcher = PATTERN_COVER_INNER.matcher(block); boolean innerCapturesFound = innerMatcher.find(); @@ -52,12 +57,13 @@ public CoverBlockProcessor(String localId, MediaFile mediaFile, return block; } - @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { + @Override + public boolean processBlockJsonAttributes(@Nullable JsonObject jsonAttributes) { JsonElement id = jsonAttributes.get("id"); - if (id != null && !id.isJsonNull() && id.getAsInt() == Integer.parseInt(mLocalId, 10)) { - addIntPropertySafely(jsonAttributes, "id", mRemoteId); + if (id != null && !id.isJsonNull() && id.getAsInt() == Integer.parseInt(localId, 10)) { + addIntPropertySafely(jsonAttributes, "id", remoteId); - jsonAttributes.addProperty("url", mRemoteUrl); + jsonAttributes.addProperty("url", remoteUrl); // check if background type is video JsonElement backgroundType = jsonAttributes.get("backgroundType"); @@ -69,7 +75,8 @@ public CoverBlockProcessor(String localId, MediaFile mediaFile, return false; } - @Override boolean processBlockContentDocument(Document document) { + @Override + public boolean processBlockContentDocument(@Nullable Document document) { // select cover block div Element targetDiv = document.selectFirst(".wp-block-cover"); @@ -78,14 +85,14 @@ public CoverBlockProcessor(String localId, MediaFile mediaFile, if (mHasVideoBackground) { Element videoElement = targetDiv.selectFirst("video"); if (videoElement != null) { - videoElement.attr("src", mRemoteUrl); + videoElement.attr("src", remoteUrl); } else { return false; } } else { // replace background-image url in style attribute String style = PATTERN_BACKGROUND_IMAGE_URL.matcher(targetDiv.attr("style")).replaceFirst( - String.format("background-image:url(%1$s)", mRemoteUrl)); + String.format("background-image:url(%1$s)", remoteUrl)); targetDiv.attr("style", style); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/FileBlockProcessor.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/FileBlockProcessor.kt index dfbaf83e2555..bc33b669ba65 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/FileBlockProcessor.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/FileBlockProcessor.kt @@ -8,14 +8,14 @@ import org.wordpress.android.util.helpers.MediaFile * When a File Block's upload is complete, this processor replaces the href pointing to a local file url with a * remote url for all a tags present within the wp:file block. */ -class FileBlockProcessor(localId: String?, mediaFile: MediaFile?) : BlockProcessor(localId, mediaFile) { +class FileBlockProcessor(localId: String, mediaFile: MediaFile) : BlockProcessor(localId, mediaFile) { override fun processBlockContentDocument(document: Document?): Boolean { val hyperLinkTargets = document?.select(HYPERLINK_TAG) hyperLinkTargets?.let { for (target in hyperLinkTargets) { // replaces the href attribute's local url with the remote counterpart. - target.attr(HREF_ATTRIBUTE, mRemoteUrl) + target.attr(HREF_ATTRIBUTE, remoteUrl) } return true } @@ -25,10 +25,10 @@ class FileBlockProcessor(localId: String?, mediaFile: MediaFile?) : BlockProcess override fun processBlockJsonAttributes(jsonAttributes: JsonObject?): Boolean { val id = jsonAttributes?.get(ID_ATTRIBUTE) - return if (id != null && !id.isJsonNull && id.asString == mLocalId) { + return if (id != null && !id.isJsonNull && id.asString == localId) { jsonAttributes.apply { - addProperty(ID_ATTRIBUTE, Integer.parseInt(mRemoteId)) - addProperty(HREF_ATTRIBUTE, mRemoteUrl) + addProperty(ID_ATTRIBUTE, Integer.parseInt(remoteId)) + addProperty(HREF_ATTRIBUTE, remoteUrl) } true } else { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java index ea62848a1d86..a1a28992c574 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java @@ -1,5 +1,8 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -33,8 +36,8 @@ public class GalleryBlockProcessor extends BlockProcessor { .append("(.*)") // inner block contents .append("(\\s*\\s*.*)").toString(), Pattern.DOTALL); - public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl, MediaUploadCompletionProcessor - mediaUploadCompletionProcessor) { + public GalleryBlockProcessor(@NonNull String localId, @NonNull MediaFile mediaFile, String siteUrl, + MediaUploadCompletionProcessor mediaUploadCompletionProcessor) { super(localId, mediaFile); mMediaUploadCompletionProcessor = mediaUploadCompletionProcessor; mGalleryImageQuerySelector = new StringBuilder() @@ -45,28 +48,29 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl mAttachmentPageUrl = mediaFile.getAttachmentPageURL(siteUrl); } - @Override boolean processBlockContentDocument(Document document) { + @Override + public boolean processBlockContentDocument(@Nullable Document document) { // select image element with our local id Element targetImg = document.select(mGalleryImageQuerySelector).first(); // if a match is found, proceed with replacement if (targetImg != null) { // replace attributes - targetImg.attr("src", mRemoteUrl); - targetImg.attr("data-id", mRemoteId); - targetImg.attr("data-full-url", mRemoteUrl); + targetImg.attr("src", remoteUrl); + targetImg.attr("data-id", remoteId); + targetImg.attr("data-full-url", remoteUrl); targetImg.attr("data-link", mAttachmentPageUrl); // replace class - targetImg.removeClass("wp-image-" + mLocalId); - targetImg.addClass("wp-image-" + mRemoteId); + targetImg.removeClass("wp-image-" + localId); + targetImg.addClass("wp-image-" + remoteId); // set parent anchor href if necessary Element parent = targetImg.parent(); if (parent != null && parent.is("a") && mLinkTo != null) { switch (mLinkTo) { case "file": - parent.attr("href", mRemoteUrl); + parent.attr("href", remoteUrl); break; case "post": parent.attr("href", mAttachmentPageUrl); @@ -83,7 +87,8 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl return false; } - @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { + @Override + public boolean processBlockJsonAttributes(@Nullable JsonObject jsonAttributes) { // The new format does not have an `ids` attributes, so returning false here will defer to recursive processing JsonArray ids = jsonAttributes.getAsJsonArray("ids"); if (ids == null || ids.isJsonNull()) { @@ -95,9 +100,9 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl } for (int i = 0; i < ids.size(); i++) { JsonElement id = ids.get(i); - if (id != null && !id.isJsonNull() && id.getAsString().equals(mLocalId)) { + if (id != null && !id.isJsonNull() && id.getAsString().equals(localId)) { try { - ids.set(i, new JsonPrimitive(Integer.parseInt(mRemoteId, 10))); + ids.set(i, new JsonPrimitive(Integer.parseInt(remoteId, 10))); } catch (NumberFormatException e) { AppLog.e(MEDIA, e.getMessage()); } @@ -107,7 +112,9 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl return false; } - @Override String processInnerBlock(String block) { + @NonNull + @Override + public String processInnerBlock(@NonNull String block) { Matcher innerMatcher = PATTERN_GALLERY_INNER.matcher(block); boolean innerCapturesFound = innerMatcher.find(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java index 3b7de9ee0f74..0811bf2ac128 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java @@ -1,5 +1,8 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -9,22 +12,23 @@ public class ImageBlockProcessor extends BlockProcessor { - public ImageBlockProcessor(String localId, MediaFile mediaFile) { + public ImageBlockProcessor(@NonNull String localId, @NonNull MediaFile mediaFile) { super(localId, mediaFile); } - @Override boolean processBlockContentDocument(Document document) { + @Override + public boolean processBlockContentDocument(@Nullable Document document) { // select image element with our local id Element targetImg = document.select("img").first(); // if a match is found, proceed with replacement if (targetImg != null) { // replace attributes - targetImg.attr("src", mRemoteUrl); + targetImg.attr("src", remoteUrl); // replace class - targetImg.removeClass("wp-image-" + mLocalId); - targetImg.addClass("wp-image-" + mRemoteId); + targetImg.removeClass("wp-image-" + localId); + targetImg.addClass("wp-image-" + remoteId); return true; } @@ -32,10 +36,11 @@ public ImageBlockProcessor(String localId, MediaFile mediaFile) { return false; } - @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { + @Override + public boolean processBlockJsonAttributes(@Nullable JsonObject jsonAttributes) { JsonElement id = jsonAttributes.get("id"); - if (id != null && !id.isJsonNull() && id.getAsString().equals(mLocalId)) { - addIntPropertySafely(jsonAttributes, "id", mRemoteId); + if (id != null && !id.isJsonNull() && id.getAsString().equals(localId)) { + addIntPropertySafely(jsonAttributes, "id", remoteId); return true; } return false; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaBlockType.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaBlockType.java index f89870f5bbc0..6e09e2155019 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaBlockType.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaBlockType.java @@ -1,5 +1,7 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import androidx.annotation.NonNull; + import org.apache.commons.lang3.StringUtils; import java.util.Arrays; @@ -66,7 +68,7 @@ static String getMatchingGroup() { * @param block The raw block contents * @return The media block type or null if no match is found */ - static MediaBlockType detectBlockType(String block) { + static MediaBlockType detectBlockType(@NonNull String block) { Matcher matcher = PATTERN_MEDIA_BLOCK_TYPES.matcher(block); if (matcher.find()) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java index f65c8500aedb..f57e6d96616c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java @@ -1,5 +1,8 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -8,22 +11,23 @@ import org.wordpress.android.util.helpers.MediaFile; public class MediaTextBlockProcessor extends BlockProcessor { - public MediaTextBlockProcessor(String localId, MediaFile mediaFile) { + public MediaTextBlockProcessor(@NonNull String localId, @NonNull MediaFile mediaFile) { super(localId, mediaFile); } - @Override boolean processBlockContentDocument(Document document) { + @Override + public boolean processBlockContentDocument(@Nullable Document document) { // select image element with our local id Element targetImg = document.select("img").first(); // if a match is found for img, proceed with replacement if (targetImg != null) { // replace attributes - targetImg.attr("src", mRemoteUrl); + targetImg.attr("src", remoteUrl); // replace class - targetImg.removeClass("wp-image-" + mLocalId); - targetImg.addClass("wp-image-" + mRemoteId); + targetImg.removeClass("wp-image-" + localId); + targetImg.addClass("wp-image-" + remoteId); // return injected block return true; @@ -34,7 +38,7 @@ public MediaTextBlockProcessor(String localId, MediaFile mediaFile) { // if a match is found for video, proceed with replacement if (targetVideo != null) { // replace attribute - targetVideo.attr("src", mRemoteUrl); + targetVideo.attr("src", remoteUrl); // return injected block return true; @@ -44,10 +48,11 @@ public MediaTextBlockProcessor(String localId, MediaFile mediaFile) { return false; } - @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { + @Override + public boolean processBlockJsonAttributes(@Nullable JsonObject jsonAttributes) { JsonElement id = jsonAttributes.get("mediaId"); - if (id != null && !id.isJsonNull() && id.getAsString().equals(mLocalId)) { - addIntPropertySafely(jsonAttributes, "mediaId", mRemoteId); + if (id != null && !id.isJsonNull() && id.getAsString().equals(localId)) { + addIntPropertySafely(jsonAttributes, "mediaId", remoteId); return true; } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessor.java index b14d6eac9f73..51f3f7790161 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessor.java @@ -1,5 +1,7 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import androidx.annotation.NonNull; + import org.wordpress.android.util.helpers.MediaFile; import java.util.regex.Matcher; @@ -19,7 +21,7 @@ public class MediaUploadCompletionProcessor { * @param mediaFile The mediaFile containing the remote id and remote url * @param siteUrl The site url - used to generate the attachmentPage url */ - public MediaUploadCompletionProcessor(String localId, MediaFile mediaFile, String siteUrl) { + public MediaUploadCompletionProcessor(@NonNull String localId, @NonNull MediaFile mediaFile, String siteUrl) { mBlockProcessorFactory = new BlockProcessorFactory(this) .init(localId, mediaFile, siteUrl); } @@ -27,7 +29,7 @@ public MediaUploadCompletionProcessor(String localId, MediaFile mediaFile, Strin /** * Processes content to replace the local ids and local urls of media with remote ids and remote urls. This method * delineates block boundaries for media-containing blocks and delegates further processing via itself and / or - * {@link #processBlock(String)}, via direct and mutual recursion, respectively. + * {@link #processBlock(String, Boolean)}, via direct and mutual recursion, respectively. * * @param content The content to be processed * @return A string containing the processed content, or the original content if no match was found @@ -77,7 +79,8 @@ public String processContent(String content) { * @param block The raw block contents * @return A string containing content with ids and urls replaced */ - private String processBlock(String block, Boolean isSelfClosingTag) { + @NonNull + private String processBlock(@NonNull String block, Boolean isSelfClosingTag) { final MediaBlockType blockType = MediaBlockType.detectBlockType(block); final BlockProcessor blockProcessor = mBlockProcessorFactory.getProcessorForMediaBlockType(blockType); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java index 1f16c8141911..ac630bdb8b80 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java @@ -1,5 +1,8 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -8,18 +11,19 @@ import org.wordpress.android.util.helpers.MediaFile; public class VideoBlockProcessor extends BlockProcessor { - public VideoBlockProcessor(String localId, MediaFile mediaFile) { + public VideoBlockProcessor(@NonNull String localId, @NonNull MediaFile mediaFile) { super(localId, mediaFile); } - @Override boolean processBlockContentDocument(Document document) { + @Override + public boolean processBlockContentDocument(@Nullable Document document) { // select video element with our local id Element targetVideo = document.select("video").first(); // if a match is found for video, proceed with replacement if (targetVideo != null) { // replace attribute - targetVideo.attr("src", mRemoteUrl); + targetVideo.attr("src", remoteUrl); // return injected block return true; @@ -28,10 +32,11 @@ public VideoBlockProcessor(String localId, MediaFile mediaFile) { return false; } - @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { + @Override + public boolean processBlockJsonAttributes(@Nullable JsonObject jsonAttributes) { JsonElement id = jsonAttributes.get("id"); - if (id != null && !id.isJsonNull() && id.getAsString().equals(mLocalId)) { - addIntPropertySafely(jsonAttributes, "id", mRemoteId); + if (id != null && !id.isJsonNull() && id.getAsString().equals(localId)) { + addIntPropertySafely(jsonAttributes, "id", remoteId); return true; } return false; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessor.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessor.kt index d1044c19abb9..1f4ef2086860 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessor.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessor.kt @@ -4,10 +4,7 @@ import com.google.gson.JsonObject import org.jsoup.nodes.Document import org.wordpress.android.util.helpers.MediaFile -class VideoPressBlockProcessor( - localId: String?, - mediaFile: MediaFile? -) : BlockProcessor(localId, mediaFile) { +class VideoPressBlockProcessor(localId: String, mediaFile: MediaFile) : BlockProcessor(localId, mediaFile) { override fun processBlockContentDocument(document: Document?): Boolean { return false } @@ -16,10 +13,10 @@ class VideoPressBlockProcessor( val id = jsonAttributes?.get(ID_ATTRIBUTE) val src = jsonAttributes?.get(SRC_ATTRIBUTE)?.asString - return if (id != null && !id.isJsonNull && id.asString == mLocalId) { + return if (id != null && !id.isJsonNull && id.asString == localId) { jsonAttributes.apply { - addIntPropertySafely(this, ID_ATTRIBUTE, mRemoteId) - addProperty(GUID_ATTRIBUTE, mRemoteGuid) + addIntPropertySafely(this, ID_ATTRIBUTE, remoteId) + addProperty(GUID_ATTRIBUTE, remoteGuid) if (src?.startsWith("file:") == true) { remove(SRC_ATTRIBUTE) } 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 index af30a157d08c..54b22755cc1a 100644 --- 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 @@ -20,6 +20,7 @@ import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.datasets.AsyncTaskHandler; import org.wordpress.android.datasets.ReaderPostTable; import org.wordpress.android.datasets.ReaderTagTable; import org.wordpress.android.fluxc.store.AccountStore; @@ -371,44 +372,47 @@ private void toggleFollowButton( return; } - final boolean isAskingToFollow = !ReaderTagTable.isFollowedTagName(currentTag.getTagSlug()); - - final String slugForTracking = currentTag.getTagSlug(); + AsyncTaskHandler.load( + () -> !ReaderTagTable.isFollowedTagName(currentTag.getTagSlug()), + isAskingToFollow -> { + final String slugForTracking = currentTag.getTagSlug(); + + ReaderActions.ActionListener listener = succeeded -> { + if (!succeeded) { + int errResId = isAskingToFollow ? R.string.reader_toast_err_adding_tag + : R.string.reader_toast_err_removing_tag; + ToastUtils.showToast(context, errResId); + } else { + if (isAskingToFollow) { + mReaderTracker.trackTag( + AnalyticsTracker.Stat.READER_TAG_FOLLOWED, + slugForTracking, + mSource + ); + } else { + mReaderTracker.trackTag( + AnalyticsTracker.Stat.READER_TAG_UNFOLLOWED, + slugForTracking, + mSource + ); + } + } + renderTagHeader(currentTag, tagHolder, true); + }; + + boolean success; + boolean isLoggedIn = mAccountStore.hasAccessToken(); + if (isAskingToFollow) { + success = ReaderTagActions.addTag(mCurrentTag, listener, isLoggedIn); + } else { + success = ReaderTagActions.deleteTag(mCurrentTag, listener, isLoggedIn); + } - ReaderActions.ActionListener listener = succeeded -> { - if (!succeeded) { - int errResId = isAskingToFollow ? R.string.reader_toast_err_adding_tag - : R.string.reader_toast_err_removing_tag; - ToastUtils.showToast(context, errResId); - } else { - if (isAskingToFollow) { - mReaderTracker.trackTag( - AnalyticsTracker.Stat.READER_TAG_FOLLOWED, - slugForTracking, - mSource - ); - } else { - mReaderTracker.trackTag( - AnalyticsTracker.Stat.READER_TAG_UNFOLLOWED, - slugForTracking, - mSource - ); + if (isLoggedIn && success) { + renderTagHeader(currentTag, tagHolder, false); + } } - } - renderTagHeader(currentTag, tagHolder, true); - }; - - boolean success; - boolean isLoggedIn = mAccountStore.hasAccessToken(); - if (isAskingToFollow) { - success = ReaderTagActions.addTag(mCurrentTag, listener, isLoggedIn); - } else { - success = ReaderTagActions.deleteTag(mCurrentTag, listener, isLoggedIn); - } - - if (isLoggedIn && success) { - renderTagHeader(currentTag, tagHolder, false); - } + ); } private void renderXPost(int position, ReaderXPostViewHolder holder) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt index ba94c014f5c5..e5b979bd9bc3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt @@ -55,11 +55,30 @@ class ReaderTopBarMenuHelper @Inject constructor( .takeIf { it.isNotEmpty() } ?.let { customListsArray -> add(MenuElementData.Divider) - add(createCustomListsItem(customListsArray)) + createCustomListsItems(customListsArray) } } } + private fun MutableList.createCustomListsItems( + customListsArray: SparseArrayCompat + ) { + if (customListsArray.size() > 2) { + // If custom lists has more than 2 items, we add a submenu called "Lists" + add(createCustomListsItem(customListsArray)) + } else { + // If the custom lists has 2 or less items, we add the items directly without submenu + customListsArray.forEach { index, readerTag -> + add( + MenuElementData.Item.Single( + id = getMenuItemIdFromReaderTagIndex(index), + text = UiString.UiStringText(readerTag.tagTitle), + ) + ) + } + } + } + private fun createDiscoverItem(id: String): MenuElementData.Item.Single { return MenuElementData.Item.Single( id = id, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/MediaUploadReadyProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/uploads/MediaUploadReadyProcessor.java index 8d72950dfa34..2df2aa732be9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/MediaUploadReadyProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/MediaUploadReadyProcessor.java @@ -1,5 +1,6 @@ package org.wordpress.android.ui.uploads; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.wordpress.android.WordPress; @@ -20,8 +21,8 @@ public class MediaUploadReadyProcessor implements MediaUploadReadyListener { } @Override - public PostModel replaceMediaFileWithUrlInPost(@Nullable PostModel post, String localMediaId, MediaFile mediaFile, - @Nullable SiteModel site) { + public PostModel replaceMediaFileWithUrlInPost(@Nullable PostModel post, @NonNull String localMediaId, + MediaFile mediaFile, @Nullable SiteModel site) { if (post != null) { boolean showAztecEditor = AppPrefs.isAztecEditorEnabled(); boolean showGutenbergEditor = AppPrefs.isGutenbergEditorEnabled(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 58f8e5854080..88edbf2e5f60 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.voicetocontent +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.model.SiteModel @@ -35,6 +36,11 @@ class VoiceToContentUseCase @Inject constructor( transcriptionResponse.model } is JetpackAITranscriptionResponse.Error -> { + val message = "${transcriptionResponse.type} ${transcriptionResponse.message}" + Log.i( + javaClass.simpleName, + "Error transcribing audio file: $message" + ) null } } diff --git a/WordPress/src/main/res/layout/item_choose_site.xml b/WordPress/src/main/res/layout/item_choose_site.xml index 9ffc791af9dd..035f6304e183 100644 --- a/WordPress/src/main/res/layout/item_choose_site.xml +++ b/WordPress/src/main/res/layout/item_choose_site.xml @@ -48,7 +48,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_extra_large" android:layout_marginVertical="@dimen/margin_medium" - app:cardCornerRadius="@dimen/avatar_sz_medium_radius" + app:cardCornerRadius="@dimen/blavatar_sz_radius" app:cardElevation="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index d12fc82577c3..b5d350c614b8 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -178,7 +178,6 @@ 24dp 32dp 40dp - 20dp 48dp 64dp 72dp @@ -188,6 +187,7 @@ 1dp 32dp 40dp + 5dp 72dp 56dp diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml index 8f766895e670..7d7245da9167 100644 --- a/WordPress/src/main/res/values/styles.xml +++ b/WordPress/src/main/res/values/styles.xml @@ -1843,6 +1843,6 @@ diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelperTest.kt index 03e57616424e..e9850dab73b1 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelperTest.kt @@ -72,6 +72,27 @@ class ReaderTopBarMenuHelperTest { assertThat(customList3Item.text).isEqualTo(UiStringText("custom-list-3")) } + @Test + fun `GIVEN custom lists has 2 items or less WHEN createMenu THEN custom lists items are shown outside a submenu`() { + val tags = ReaderTagList().apply { + add(mockFollowingTag()) // item 0 + add(mockDiscoverTag()) // item 1 + add(mockSavedTag()) // item 2 + add(mockLikedTag()) // item 3 + add(mockA8CTag()) // item 4 + add(mockFollowedP2sTag()) // item 5 + add(createCustomListTag("custom-list-1")) // item 6 + add(createCustomListTag("custom-list-2")) // item 7 + } + val menu = helper.createMenu(tags) + + val customListItem1 = menu.findSingleItem { it.id == "6" }!! + assertThat(customListItem1.text).isEqualTo(UiStringText("custom-list-1")) + + val customListItem2 = menu.findSingleItem { it.id == "7" }!! + assertThat(customListItem2.text).isEqualTo(UiStringText("custom-list-2")) + } + @Test fun `GIVEN all tags are available and tags FF enabled WHEN createMenu THEN all items are created correctly`() { whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(true) diff --git a/build.gradle b/build.gradle index 6e2ee16bf21d..58a456beb870 100644 --- a/build.gradle +++ b/build.gradle @@ -15,17 +15,17 @@ plugins { ext { minSdkVersion = 24 compileSdkVersion = 34 - targetSdkVersion = 33 + targetSdkVersion = 34 } ext { // libs automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' - automatticTracksVersion = '5.0.0' - gutenbergMobileVersion = 'v1.120.0-alpha1' + automatticTracksVersion = '5.1.0' + gutenbergMobileVersion = 'v1.120.0' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = 'trunk-a9a471914d3ebd1093986d3d0a95c2a29b29dca0' + wordPressFluxCVersion = '2.83.0' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/AztecEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/AztecEditorFragment.java index 836c3e67a011..17537f28195d 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/AztecEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/AztecEditorFragment.java @@ -2127,7 +2127,7 @@ private static void clearMetaSpans(Spannable text) { } public static String replaceMediaFileWithUrl(Context context, @NonNull String postContent, - String localMediaId, MediaFile mediaFile) { + @NonNull String localMediaId, MediaFile mediaFile) { if (mediaFile != null) { String remoteUrl = StringUtils.notNullStr(Utils.escapeQuotes(mediaFile.getFileURL())); // fill in Aztec with the post's content