Skip to content

Commit b6653d1

Browse files
authored
fix: Prevent multiple Android WebView starts (#175)
* chore: Ignore Kotlin build directory * fix: Disable rudimentary WebView preloading This load did not consider the editor configuration, meaning it often loaded the incorrect URL. Additionally, because the editor reveal logic currently only runs on first load, this preload intermittently led to an invisible editor; where the first load revealed the editor and the second, configured load hid the editor. 1. Preload 2. Hide editor 3. Load URL 4. Reveal editor 5. Start editor with configuration 6. Hide editor 7. Load URL * feat: Expand Android warmup to load all editor assets Load and cache not only the manifest, but the editor assets themselves. * refactor: De-duplicate Android editor load listeners It appears we added multiple listeners for the same event at some point. Also, we did not clean up one of the two listeners during tear down. * refactor: Extracted magic number to named constant * fix: Replace WebView pool with warmup utilities the benefits of pooling WebViews became moot once we stopped loading a URL during the initialization sequence. It now provides no benefit. Additionally, the pooled WebViews conflicted with the new warmup utilities. It was possible for an editor launch to reuse an in-progress warmup WebView. Once the warmup destroyed its WebView, the editor began failing.
1 parent c6aca00 commit b6653d1

File tree

3 files changed

+55
-129
lines changed

3 files changed

+55
-129
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ dist
182182
local.properties
183183
/android/build
184184
/android/.gradle
185+
/android/.kotlin
185186
/android/.idea
186187

187188
## Production Build Products

android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ import androidx.webkit.WebViewAssetLoader.AssetsPathHandler
2626
import org.json.JSONException
2727
import org.json.JSONObject
2828
import java.util.Locale
29-
import kotlinx.coroutines.CoroutineScope
30-
import kotlinx.coroutines.Dispatchers
31-
import kotlinx.coroutines.Job
32-
import kotlinx.coroutines.SupervisorJob
33-
import kotlinx.coroutines.cancelChildren
34-
import kotlinx.coroutines.launch
3529

3630
const val ASSET_URL = "https://appassets.androidplatform.net/assets/index.html"
3731
const val ASSET_URL_REMOTE = "https://appassets.androidplatform.net/assets/remote.html"
@@ -45,7 +39,6 @@ class GutenbergView : WebView {
4539
private var configuration: EditorConfiguration = EditorConfiguration.builder().build()
4640

4741
private val handler = Handler(Looper.getMainLooper())
48-
private var editorDidBecomeAvailable: ((GutenbergView) -> Unit)? = null
4942
var filePathCallback: ValueCallback<Array<Uri?>?>? = null
5043
val pickImageRequestCode = 1
5144

@@ -461,7 +454,6 @@ class GutenbergView : WebView {
461454
handler.post {
462455
if(!didFireEditorLoaded) {
463456
editorDidBecomeAvailableListener?.onEditorAvailable(this)
464-
this.editorDidBecomeAvailable?.let { it(this) }
465457
this.didFireEditorLoaded = true
466458
this.visibility = View.VISIBLE
467459
this.alpha = 0f
@@ -575,7 +567,7 @@ class GutenbergView : WebView {
575567
contentChangeListener = null
576568
historyChangeListener = null
577569
featuredImageChangeListener = null
578-
editorDidBecomeAvailable = null
570+
editorDidBecomeAvailableListener = null
579571
filePathCallback = null
580572
onFileChooserRequested = null
581573
autocompleterTriggeredListener = null
@@ -584,64 +576,75 @@ class GutenbergView : WebView {
584576
}
585577

586578
companion object {
587-
private val warmupScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
579+
private const val ASSET_LOADING_TIMEOUT_MS = 5000L
580+
581+
// Warmup state management
582+
private var warmupHandler: Handler? = null
583+
private var warmupRunnable: Runnable? = null
584+
private var warmupWebView: GutenbergView? = null
588585

589586
/**
590-
* Warmup the editor by preloading manifest
587+
* Warmup the editor by preloading assets in a temporary WebView.
588+
* This pre-caches assets to improve editor launch speed.
591589
*/
592590
@JvmStatic
593591
fun warmup(context: Context, configuration: EditorConfiguration) {
594-
if (configuration.enableAssetCaching) {
595-
val library = EditorAssetsLibrary(context, configuration)
596-
// Preload manifest in background
597-
warmupScope.launch {
598-
try {
599-
library.manifestContentForEditor()
600-
} catch (e: Exception) {
601-
Log.e("GutenbergView", "Failed to warmup manifest", e)
602-
}
603-
}
592+
// Cancel any existing warmup
593+
cancelWarmup()
594+
595+
// Create dedicated warmup WebView
596+
val webView = GutenbergView(context)
597+
webView.initializeWebView()
598+
webView.start(configuration)
599+
warmupWebView = webView
600+
601+
// Schedule cleanup after assets are loaded
602+
warmupHandler = Handler(Looper.getMainLooper())
603+
warmupRunnable = Runnable {
604+
cleanupWarmup()
604605
}
606+
warmupHandler?.postDelayed(warmupRunnable!!, ASSET_LOADING_TIMEOUT_MS)
605607
}
606608

607609
/**
608-
* Cancel any ongoing warmup operations
610+
* Cancel any pending warmup and clean up resources.
609611
*/
610612
@JvmStatic
611613
fun cancelWarmup() {
612-
warmupScope.coroutineContext[Job]?.cancelChildren()
614+
warmupRunnable?.let { runnable ->
615+
warmupHandler?.removeCallbacks(runnable)
616+
}
617+
cleanupWarmup()
613618
}
614-
}
615-
}
616-
617-
object GutenbergWebViewPool {
618-
private var preloadedWebView: GutenbergView? = null
619619

620-
@JvmStatic
621-
fun getPreloadedWebView(context: Context): GutenbergView {
622-
val currentView = preloadedWebView
623-
if (currentView == null) {
624-
preloadedWebView = createAndPreloadWebView(context)
625-
} else {
626-
(currentView.parent as? android.view.ViewGroup)?.removeView(currentView)
620+
/**
621+
* Clean up warmup resources.
622+
*/
623+
private fun cleanupWarmup() {
624+
warmupWebView?.let { webView ->
625+
webView.stopLoading()
626+
webView.clearConfig()
627+
webView.destroy()
628+
}
629+
warmupWebView = null
630+
warmupHandler = null
631+
warmupRunnable = null
627632
}
628-
return preloadedWebView!!
629-
}
630-
631-
private fun createAndPreloadWebView(context: Context): GutenbergView {
632-
val webView = GutenbergView(context)
633-
webView.initializeWebView()
634-
webView.loadUrl(ASSET_URL)
635-
return webView
636-
}
637633

638-
@JvmStatic
639-
fun recycleWebView(webView: GutenbergView) {
640-
webView.stopLoading()
641-
webView.clearConfig()
642-
webView.removeAllViews()
643-
webView.destroy()
644-
preloadedWebView = null
634+
/**
635+
* Create a new GutenbergView for the editor.
636+
* Cancels any pending warmup to free resources.
637+
*/
638+
@JvmStatic
639+
fun createForEditor(context: Context): GutenbergView {
640+
// Cancel any pending warmup to free resources
641+
cancelWarmup()
642+
643+
// Create fresh WebView for editor
644+
val webView = GutenbergView(context)
645+
webView.initializeWebView()
646+
return webView
647+
}
645648
}
646649
}
647650

android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergWebViewPoolTest.kt

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)