Android Asset Caching Implementation#153
Conversation
This commit adds asset caching support for Android that mirrors the iOS implementation from PR #148, while leveraging Android's native capabilities to avoid URL scheme modification. - **EditorAssetsLibrary**: Manages fetching manifest from `wpcom/v2/editor-assets` endpoint and caching individual JS/CSS assets - **EditorAssetsManifest**: Parses HTML content from manifest using JSoup to extract asset URLs - **EditorAssetsProvider**: JavaScript interface that provides manifest to WebView via `loadFetchedEditorAssets` - **CachedAssetRequestInterceptor**: Intercepts and caches asset requests using the existing `GutenbergRequestInterceptor` interface - Added `editorAssetsEndpoint` property to `EditorConfiguration` - Added `enableAssetCaching` flag to enable/disable caching - Added `cachedAssetHosts` to specify which hosts to cache from - Updated `GutenbergView` to set up caching when enabled - Added `warmup()` method for preloading assets - Integrated JavaScript bridge support for both iOS and Android in `bridge.js` - Added JSoup dependency for HTML parsing - No URL scheme modification needed - Android intercepts requests directly - Uses existing `GutenbergRequestInterceptor` infrastructure - Simpler implementation while maintaining feature parity - 7-day cache expiration (matching iOS) - Cache directory structure matches iOS pattern - Only caches successful responses (2xx status codes) - Supports .js, .css, and .js.map files 🤖 Generated with [Claude Code](https://claude.ai/code)
Replace direct readText() call on InputStream with bufferedReader().readText() to properly convert the stream to text content when fetching the manifest.
Removes the JavaScript interface approach in favor of direct request interception, making the Android implementation cleaner while maintaining the same functionality. - Remove EditorAssetsProvider JavaScript interface - Update CachedAssetRequestInterceptor to intercept manifest endpoint requests - Implement fetchEditorAssets() in bridge.js for Android using apiFetch - Simplify remote-editor.js to use fetchEditorAssets() for both platforms - Add header forwarding support for manifest requests Now Android intercepts the /wpcom/v2/editor-assets API call directly instead of using a JavaScript bridge, leveraging Android's native request interception capabilities while maintaining feature parity with iOS.
Simplifies the Android implementation by removing HTML parsing since Android doesn't need to modify URLs like iOS does. Android can intercept requests directly without URL scheme changes. - Remove EditorAssetsManifest.kt class and JSoup dependency - Simplify manifestContentForEditor() to return raw JSON without parsing - Remove fetchAssets() method - assets are cached on-demand when requested - Update warmup() to only preload manifest, not individual assets - Remove Gson import and JSON parsing logic - Significantly reduced complexity and dependencies - More efficient - no unnecessary HTML parsing or JSON manipulation - Leverages Android's native request interception capabilities - Assets are cached lazily as they're actually requested by the editor The Android implementation now simply: 1. Intercepts /wpcom/v2/editor-assets → returns raw JSON 2. Intercepts asset requests → caches and serves them 3. No URL modification or HTML parsing required
Replace runBlocking calls in WebView request interceptor with non-blocking approach: - Only serve assets that are already cached - Start background caching for future requests when asset not found - Let WebView handle manifest and uncached requests normally - Prevents ANR issues while maintaining caching functionality
Add try-finally blocks around HttpURLConnection usage to guarantee connection.disconnect() is called in all scenarios including: - Network timeouts and errors - HTTP error responses - IOException during data reading - Any other exceptions Prevents accumulation of unclosed connections that could exhaust system resources, especially important on mobile devices.
…management - Replace deprecated GlobalScope.launch with proper CoroutineScope - Use SupervisorJob + Dispatchers.IO for isolated error handling and IO optimization - Add cancelWarmup() method for explicit control over background operations - Add explicit coroutines dependency and replace star imports with specific imports - Prevents memory leaks and provides proper lifecycle management for warmup operations
Implements periodic cleanup of old cached assets to prevent unlimited storage growth. Since assets are versioned with URLs that change when updated, old versions become unused naturally. The cleanup removes files older than 7 days and provides logging for monitoring.
- Update fetchEditorAssets() to use editorAssetsEndpoint if configured - Add editorAssetsEndpoint to GBKit JavaScript configuration - Match the native Android implementation's fallback logic - Ensure consistent URL construction between native and bridge code
|
Thank you! This caching will have a big impact on performance and stability. While testing this, I discovered I recently shipped a regression to WPCOM Simple sites that broke loading of Jetpack blocks in GBK. During your own testing, if you ever successfully loaded the editor for a WPCOM Simple site and found no Jetpack blocks, this regression was likely the cause. I corrected this in Automattic/jetpack#44274. Successful testingAfter fixing the regression, I successfully loaded Jetpack blocks using the caching mechanism—both WPCOM Simple and Atomic sites. I used the following patch for WP-Android to ensure the correct configuration. WP-Android cache configuration diffdiff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt
index 135861253d6..6c9e98c611d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt
@@ -2520,6 +2520,7 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor
)
val postType = if (editPostRepository.isPage) "page" else "post"
+ // TODO: Update this to set the correct siteApiRoot for self-hosted sites
val siteApiRoot = if (isWpCom) "https://public-api.wordpress.com/" else ""
val authToken = accountStore.accessToken
val authHeader = "Bearer $authToken"
@@ -2533,6 +2534,7 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor
"postType" to postType,
"postTitle" to editPostRepository.getPost()?.title,
"postContent" to editPostRepository.getPost()?.content,
+ "siteURL" to siteModel.url,
"siteApiRoot" to siteApiRoot,
"namespaceExcludedPaths" to arrayOf("/wpcom/v2/following/recommendations", "/wpcom/v2/following/mine"),
"authHeader" to authHeader,
diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergKitEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergKitEditorFragment.java
index 4686f4d3b79..5f41b0ccd50 100644
--- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergKitEditorFragment.java
+++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergKitEditorFragment.java
@@ -41,6 +41,7 @@ import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
import org.wordpress.android.util.PermissionUtils;
import org.wordpress.android.util.ProfilingUtils;
+import org.wordpress.android.util.UrlUtils;
import org.wordpress.android.util.helpers.MediaFile;
import org.wordpress.android.util.helpers.MediaGallery;
import org.wordpress.aztec.IHistoryListener;
@@ -58,6 +59,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import static org.wordpress.gutenberg.Media.createMediaUsingMimeType;
@@ -565,13 +567,19 @@ public class GutenbergKitEditorFragment extends EditorFragmentAbstract implement
postId = -1;
}
+ var siteURL = (String) mSettings.get("siteURL");
+ var siteApiRoot = (String) mSettings.get("siteApiRoot");
+ var siteApiNamespace = (String[]) mSettings.get("siteApiNamespace");
+ // TODO: Update to set correct endpoint for self-hosted sites
+ var editorAssetsEndpoint = siteApiRoot + "wpcom/v2/" + siteApiNamespace[0] + "/editor-assets";
+
EditorConfiguration config = new EditorConfiguration.Builder()
.setTitle((String) mSettings.get("postTitle"))
.setContent((String) mSettings.get("postContent"))
.setPostId(postId)
.setPostType((String) mSettings.get("postType"))
.setThemeStyles((Boolean) mSettings.get("themeStyles"))
- .setPlugins((Boolean) mSettings.get("plugins"))
+ .setPlugins((Boolean) true)
.setSiteApiRoot((String) mSettings.get("siteApiRoot"))
.setSiteApiNamespace((String[]) mSettings.get("siteApiNamespace"))
.setNamespaceExcludedPaths((String[]) mSettings.get("namespaceExcludedPaths"))
@@ -579,6 +587,9 @@ public class GutenbergKitEditorFragment extends EditorFragmentAbstract implement
.setWebViewGlobals((List<WebViewGlobal>) mSettings.get("webViewGlobals"))
.setEditorSettings(editorSettings)
.setLocale((String) mSettings.get("locale"))
+ .setEditorAssetsEndpoint(editorAssetsEndpoint)
+ .setCachedAssetHosts(Set.of("s0.wp.com", UrlUtils.getHost(siteURL)))
+ .setEnableAssetCaching(true)
.build();
mGutenbergView.start(config);
You are correct. In WP-iOS, we set the correct API root for self-hosted sites; we need to implement the same in WP-Android now that application passwords are available.
They do, but it requires the Jetpack plugin to be installed and activated, as the editor assets endpoint is not yet available in WordPress Core. The endpoint URL would resemble SuggestionsWe might consider implementing similar navigation as the iOS GBK Demo app, where one can test either the bundled editor a configured remote editor. iOS GBK Demo app navigation
gbk-ios-editors-navigation.mp4Given my success testing the current changes, I suggest we move forward refining the code in preparation for review. Please let me know if there are any aspects with which you would like my help. |
This suggestion has been implemented in #155 with a minor follow up fix in #156. |
…endency Replaced order-dependent map lookup with explicit `when` expression to determine mime types. This eliminates the risk of incorrect mime type matching when `.css` would match before `.css?inline`. - Remove `MIME_TYPES` constant in favor of `getMimeType()` function - Add comment explaining why `.css?inline` files are served as `application/javascript` - Reference `use-editor-styles.js` where Vite transforms CSS files with `?inline` parameter into JavaScript modules that export CSS as strings
87a986b to
33a5382
Compare
|
@dcalhoun This is now ready for review. The force-push was about me trying to fix the lint error, so there are no changes to the existing commits from before your first review. |
android/Gutenberg/src/main/java/org/wordpress/gutenberg/CachedAssetRequestInterceptor.kt
Show resolved
Hide resolved
| private var enableAssetCaching: Boolean = false | ||
| private var cachedAssetHosts: Set<String> = emptySet() |
There was a problem hiding this comment.
The iOS implementation lacks these options. Do you believe we should add them to iOS?
There was a problem hiding this comment.
I am not sure about cachedAssetHosts, but I think enableAssetCaching is useful. It's much easier to turn off a flag for debugging than to have to do an included build, or publish a different version of the library to disable caching.
What do you think @crazytonyli?
There was a problem hiding this comment.
I believe we should use the same flag for "asset caching" and "is using remote editor", because we'd always want to cache assets for remote editors.
There is also an editorAssetsEndpoint property, which can be used as a flag. Asset caching is only possible if that property is set.
There was a problem hiding this comment.
@crazytonyli I had similar thoughts regarding using editorAssetsEndpoint as both the toggle for plugins and the configuration for fetching remote editor assets. My only concern is naming it in a way that clearly communicates its intent and impact. Should we keep editorAssetsEndpoint or name it something else—remoteEditorAssetsEndpoint, pluginAssetsEndpoint, etc? It returns both plugin scripts and core WP scripts.
My understanding is that editorAssetsEndpoint is currently not required as both platforms have a fallback value of <site-host>/wp-json/wpcom/v2/editor-assets. I am in favor of removing the fallback and requiring a configured path. This would allow use to remove the wpcom reference from this codebase that we hope to merge into the community-centric Gutenberg project.
There was a problem hiding this comment.
I believe we should use the same flag for "asset caching" and "is using remote editor", because we'd always want to cache assets for remote editors.
That may be true for production, but I don't agree with it for debugging. It's a good idea to have a way to turn something off without having to change the source code.
One improvement that might address our concerns is to group caching-related properties into a single container object. Currently, caching arguments are scattered as separate parameters, making their relationships unclear without prior knowledge.
With a container approach, we can:
- Use constructors that automatically set sensible defaults
- Set some properties based on others internally
- Reduce the number of constructor arguments
class CacheConfig {
constructor(endpoint) {
this.enabled = true;
this.endpoint = endpoint;
}
}
Summary
This PR implements the Android counterpart for iOS editor asset caching (PR #148), enabling performance improvements for remote editor functionality by caching JavaScript, CSS, and source map files locally.
Implementation Overview
Key Components
CachedAssetRequestInterceptor- Intercepts WebView requests for cacheable assetsEditorAssetsLibrary- Manages HTTP operations, caching, and cleanupEditorConfiguration- Extended with caching-related propertiesArchitecture Decisions
Technical Details
Caching Strategy
.js,.css,.js.mapfiles from configured hostscontext.filesDir/editor-caches/{site-name}(matches iOS structure)Configuration
URL Construction
Matches iOS implementation:
editorAssetsEndpointif configured{siteApiRoot}wpcom/v2/editor-assets