-
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
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
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.
|
@jkmassel I noticed a couple of things which may or may not be a problem. First, I tried to add a Jurassic Ninja site that doesn't have the Gutenberg plugin, and I got this error when starting the editor:
I'm not sure if this error would occur in a production app, but if so we could stand to add some padding to it. Next, I tried to add a wp.com simple site that does have Gutenberg, and I see the "Discovering site" message but it never gets added. not-added.mp4Also, I realize this is just a demo app, but it would still be nice to have feedback (like a "Cache cleared" toast) when tapping "Clear preload cache." When I clicked it I assumed it didn't do anything, but looking at the code I see it does. |
Full warning: > Redundant creation of Json format. Creating instances for each usage can be slow.
84042ab to
bce9aad
Compare
|
@nbradbury's issue is fixed for iOS in #288. This PR is already pretty huge, so I'll open a follow-up to address it there. |

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.