-
Notifications
You must be signed in to change notification settings - Fork 3
Add Android Preload List #260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Conversation
| val viewModel = ViewModelProvider( | ||
| this, | ||
| SitePreparationViewModelFactory(application, configurationItem) | ||
| )[SitePreparationViewModel::class.java] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⛏️Aren't we using injection in the example project? ViewModels can be simply injected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we are at the moment – I'd be happy to adopt it, but I think it's outside the scope of this PR :)
android/Gutenberg/src/main/java/org/wordpress/gutenberg/model/EditorDependenciesSerializer.kt
Outdated
Show resolved
Hide resolved
android/Gutenberg/src/main/java/org/wordpress/gutenberg/model/EditorSettings.kt
Outdated
Show resolved
Hide resolved
android/Gutenberg/src/main/java/org/wordpress/gutenberg/stores/EditorAssetsLibrary.kt
Outdated
Show resolved
Hide resolved
| responseHeaders = EditorHTTPHeaders(entry.headers) | ||
| ) | ||
| } catch (e: Exception) { | ||
| null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to track the exception here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case probably not, because we'll treat a cache failure as a simple cache miss. I've modified this to use runCatching in c64592b, which should hopefully make the intent more clear!
android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt
Outdated
Show resolved
Hide resolved
209696f to
f59acb1
Compare
f59acb1 to
c82027d
Compare
0e760f1 to
8060669
Compare
86161e3 to
0d0ce7f
Compare
Full warning: > Redundant creation of Json format. Creating instances for each usage can be slow.
bd9046e to
84042ab
Compare
|
Claude finished @dcalhoun's task in 3m 13s —— View job Comprehensive Code Review Complete ✅I've completed a thorough review of PR #260: Add Android Preload List. This is an excellent implementation that brings feature parity with iOS. Below is my detailed analysis across all requested areas. Executive SummaryOverall Assessment: ✅ Approved with Minor Recommendations This is a well-architected, thoroughly tested implementation. The code quality is high, security practices are sound, and the test coverage is excellent. I identified a few minor areas for potential improvement, but none are blocking issues. Key Strengths:
1. Code Quality ✅Strengths
ObservationsFunction ordering follows project conventions (android/Gutenberg/src/main/java/org/wordpress/gutenberg/services/EditorService.kt:260-374): Excellent use of companion objects for factory methods (android/Gutenberg/src/main/java/org/wordpress/gutenberg/services/EditorService.kt:71-169): Minor: Consider extracting magic numbers (android/Gutenberg/src/main/java/org/wordpress/gutenberg/services/EditorService.kt:73): private const val CLEANUP_INTERVAL_SECONDS = 86_400L // 1 dayThis is good, but there are a few other magic numbers that could benefit from named constants:
2. Security 🔒Strengths
RecommendationsConsider sanitizing logging output (android/Gutenberg/src/main/java/org/wordpress/gutenberg/EditorHTTPClient.kt:154, 201): Log.e(TAG, "HTTP error downloading $url: $statusCode")
Log.e(TAG, "HTTP error fetching $url: $statusCode")URLs might contain sensitive query parameters (tokens, etc.). Consider redacting query params in logs: Log.e(TAG, "HTTP error downloading ${url.substringBefore('?')}: $statusCode")WordPress error parsing is defensive (android/Gutenberg/src/main/java/org/wordpress/gutenberg/EditorHTTPClient.kt:227-243) ✅: 3. Performance ⚡StrengthsConcurrent downloads (android/Gutenberg/src/main/java/org/wordpress/gutenberg/stores/EditorAssetsLibrary.kt:194-211): coroutineScope {
val downloads = assets.map { assetUrl ->
async {
fetchAsset(assetUrl, bundle)
mutex.withLock {
completed++
progress?.invoke(EditorProgress(completed = completed, total = assets.size))
}
}
}
downloads.awaitAll()
}Excellent parallel downloading of assets with proper progress tracking ✅ Smart caching strategy:
Memory-conscious design (android/Gutenberg/src/main/java/org/wordpress/gutenberg/EditorHTTPClient.kt:190-194): // Note: This loads the entire response into memory. This is acceptable because
// this method is only used for WordPress REST API responses (editor settings, post
// data, themes, etc.) which are expected to be small (KB range). Large assets like
// JS/CSS files use the download() method which streams directly to disk.Great comment explaining the design decision. Large files are streamed to disk, small API responses are loaded into memory. Progress tracking is granular (android/Gutenberg/src/main/java/org/wordpress/gutenberg/services/EditorService.kt:57-68): ObservationsThread-safe caching (android/Gutenberg/src/main/java/org/wordpress/gutenberg/stores/EditorURLCache.kt:30): Potential optimization: Consider adding a memory cache layer for frequently accessed items in 4. Testing 🧪StrengthsComprehensive coverage: 19 test files covering all major components:
Excellent test quality (android/Gutenberg/src/test/java/org/wordpress/gutenberg/EditorHTTPClientTest.kt):
Test utilities (android/Gutenberg/src/test/java/org/wordpress/gutenberg/services/EditorServiceTest.kt:133-221): ObservationsTest coverage appears excellent: Every new class has a corresponding test file. The tests cover:
Missing: Integration tests for the full prepare → load flow. While unit tests are comprehensive, an end-to-end test that exercises the full flow from 5. Documentation 📚StrengthsExcellent KDoc coverage: All public APIs have clear documentation with:
PR description is exemplary: The PR body clearly explains:
Code comments explain "why" not "what": // Note: This loads the entire response into memory. This is acceptable because...Comments focus on design decisions and rationale, not just describing code. RecommendationsREADME update needed: The main README doesn't appear to have been updated to document the new preloading system. Consider adding:
Demo app is well-implemented: The Specific File ReviewsEditorService.kt ⭐ Excellent
EditorHTTPClient.kt ✅ Good
EditorAssetsLibrary.kt ⭐ Excellent
EditorURLCache.kt ✅ Good
GutenbergView.kt ✅ Good
Model Classes ✅ Good
Addressing Previous Review CommentsAll previous review comments from @adalpari have been properly addressed:
Final RecommendationsMust AddressNone - no blocking issues found. Should Consider
Nice to Have
ConclusionThis is an excellent implementation that I'm happy to approve. The code quality is high, the architecture is sound, testing is comprehensive, and the security practices are solid. The recommendations above are minor improvements that don't block merging. The PR successfully achieves its goals:
Great work! 🎉 |
| val headers = extractHeaders(response) | ||
|
|
||
| if (statusCode !in 200..299) { | ||
| Log.e(TAG, "HTTP error downloading $url: $statusCode") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security consideration: URLs might contain sensitive query parameters (auth tokens, etc.). Consider sanitizing URLs in logs to prevent credential leakage:
val sanitizedUrl = url.substringBefore('?')
Log.e(TAG, "HTTP error downloading $sanitizedUrl: $statusCode")This applies to lines 154, 201, and any other places where URLs are logged.
| * @return The file where the asset is (or should be) stored. | ||
| * @throws IllegalArgumentException if the URL path would escape the bundle root. | ||
| */ | ||
| fun assetDataPath(url: String): File { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Excellent path traversal protection ✅
This code properly prevents directory traversal attacks by:
- Canonicalizing both the resolved file and bundle root
- Checking that the resolved path is within the bundle root
- Throwing an exception if the path escapes the bundle
This is a critical security control for handling untrusted asset URLs.
| ) | ||
| bundle.setEditorRepresentation(editorRepresentation) | ||
|
|
||
| // Download all assets concurrently. If any download fails, the entire operation fails |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Performance: Excellent concurrent downloads ⚡
This implementation properly:
- Downloads assets in parallel using
async - Protects shared state (progress) with a mutex
- Uses
awaitAll()to fail-fast if any download fails - Reports granular progress to the user
The fail-fast approach (line 196 comment) is the right choice - a partial bundle would result in a broken editor.
| /** | ||
| * Builds a unique cache key for the given URL and HTTP method combination. | ||
| */ | ||
| private fun buildCacheKey(url: String, httpMethod: EditorHttpMethod): String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Good cache key design ✅
Using SHA-256 to hash method:url prevents:
- Cache key collisions
- Path traversal via cache keys (URLs could contain
../) - Information leakage through predictable cache file names
Consider: For additional security, you could include the site ID in the hash to ensure complete isolation between different sites.
| * @param handle A unique identifier for this operation. | ||
| * @param work The work to execute. | ||
| */ | ||
| private fun onceEvery(intervalSeconds: Long, handle: String, work: () -> Unit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: Silent failure when SharedPreferences is null
If sharedPreferences is null (line 384), the onceEvery logic silently returns without executing the work. This is fine for the cleanup() use case, but consider logging when this happens to aid debugging:
private fun onceEvery(intervalSeconds: Long, handle: String, work: () -> Unit) {
val prefs = sharedPreferences
if (prefs == null) {
Log.d(TAG, "SharedPreferences not available, skipping onceEvery for: $handle")
return
}
// ... rest of implementation
}| * }) | ||
| * ``` | ||
| * | ||
| * ## Coroutine Scope Requirements |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documentation: Excellent coroutine scope ownership explanation ✅
This documentation clearly explains:
- The caller owns the coroutine scope
- Why lifecycle-aware scopes should be used
- That the view doesn't cancel the scope
This addresses the earlier review concern from @adalpari and prevents lifecycle bugs. Great improvement!
dcalhoun
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This look really sound—nicely done!
I spent most of my time reviewing the logic itself, and little time on the automated tests.
I tested the functionality in the demo app. Testing went well. The performance improvement is notable on sites like slow.wpmt.co. For other sites, there a notable delay remains during editor start (after preloading) that presumably stems from time spent loading editor/plugin assets. My understanding is that we might further optimize those in the future with things like referenced prerenderUrlAsync.
I feel Claude's review is accurate, and the inline comments are likely worth considering. Outside of that, I noted a few oddities in my own inline comments to consider. None of these appear as blockers, some only impact the demo app.
@jkmassel let me know what you think about Claude's and my review and if you'd like for me to approve as-is.
An aside: with the editor settings (background color) now applied to the Android demo app, I noted the editor height is unexpectedly cut off. I addressed this in #275.
| item { | ||
| DependenciesStatusCard( | ||
| hasDependencies = uiState.editorDependencies != null | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't appear to correctly denote the presence of caches created from loading the editor (i.e., without using the dedicated "prepare" buttons). It also does not recognize old caches—navigating here, preparing a cache, navigating back to the root view, and re-navigating here results in an inaccurate "editor dependencies missing."
Additionally, in the latter scenario, the loading view incorrect displays "Loading Editor" rather than "Starting Editor." The progress bar displayed also never updates after initial render in this context.
Unrecognized preload caches recording
gbk-android-preloading-cache.mp4
| } | ||
|
|
||
| class Builder { | ||
| class Builder(private var siteURL: String, private var siteApiRoot: String, private var postType: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I perceive this makes these values required by the editor configuration. The optional values were useful for using the block editor for non-site-specific contexts in the future—e.g., editing comment text in the Jetpack app, editing entries in the Day One app. However, I perceive this can still be accomplished with something like the following. 👍🏻
val config = EditorConfiguration.bundled()
.toBuilder()
.setContent("my content here")
.build()| */ | ||
| suspend fun fetchPost(id: Int): EditorURLResponse { | ||
| val url = buildPostUrl(id) | ||
| val response = httpClient.perform("GET", url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why might this and other similar lines use a string "GET" rather than EditorHttpMethod.GET? This doesn't appear to use the internal helper—should it?
| // Create progress bar | ||
| progressBar = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal).apply { | ||
| layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { | ||
| marginStart = dpToPx(16) | ||
| marginEnd = dpToPx(16) | ||
| } | ||
| max = 100 | ||
| progress = 0 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.

What?
Adds an Android preloading system for the Gutenberg editor that prefetches editor dependencies before display, matching the existing iOS implementation.
Why?
The Gutenberg editor requires several API calls and asset downloads during initialization, which can cause slow launch times. This PR introduces a preloading architecture that:
How?
New Core Components:
EditorService- Orchestrates loading all editor dependencies with progress trackingEditorPreloadList- Pre-fetched API responses injected into the editor JavaScriptEditorAssetsLibrary(in stores/) - Downloads and caches asset bundlesEditorHTTPClient- Authenticated HTTP client for WordPress REST APIRESTAPIRepository- REST API calls with response cachingEditorURLCache- URL-level caching with configurable policiesNew Model Classes:
EditorAssetBundle- Represents downloaded editor assetsEditorCachePolicy- Configurable caching strategies (Always, MaxAge, Ignore)EditorSettings,EditorDependencies- Editor configuration modelsGBKitGlobal,LocalEditorAssetManifest,RemoteEditorAssetManifest- Asset manifest parsingDemo App Updates:
SitePreparationActivityandSitePreparationViewModelshowing the preloading flow with progress UIOther Changes:
EditorSettingsimprovements for consistencyandroidx.webkitexposesprerenderUrlAsyncwhich we should adopt for this purpose.Testing Instructions
make test-androidto verify all new unit tests passSee the tests in #250 for examples of tests sites you can use.