diff --git a/.gitignore b/.gitignore index e91cb855..210dc00a 100644 --- a/.gitignore +++ b/.gitignore @@ -182,6 +182,7 @@ dist local.properties /android/build /android/.gradle +/android/.kotlin /android/.idea ## Production Build Products diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index 8a3f9356..0a021535 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -26,12 +26,6 @@ import androidx.webkit.WebViewAssetLoader.AssetsPathHandler import org.json.JSONException import org.json.JSONObject import java.util.Locale -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch const val ASSET_URL = "https://appassets.androidplatform.net/assets/index.html" const val ASSET_URL_REMOTE = "https://appassets.androidplatform.net/assets/remote.html" @@ -45,7 +39,6 @@ class GutenbergView : WebView { private var configuration: EditorConfiguration = EditorConfiguration.builder().build() private val handler = Handler(Looper.getMainLooper()) - private var editorDidBecomeAvailable: ((GutenbergView) -> Unit)? = null var filePathCallback: ValueCallback?>? = null val pickImageRequestCode = 1 @@ -461,7 +454,6 @@ class GutenbergView : WebView { handler.post { if(!didFireEditorLoaded) { editorDidBecomeAvailableListener?.onEditorAvailable(this) - this.editorDidBecomeAvailable?.let { it(this) } this.didFireEditorLoaded = true this.visibility = View.VISIBLE this.alpha = 0f @@ -575,7 +567,7 @@ class GutenbergView : WebView { contentChangeListener = null historyChangeListener = null featuredImageChangeListener = null - editorDidBecomeAvailable = null + editorDidBecomeAvailableListener = null filePathCallback = null onFileChooserRequested = null autocompleterTriggeredListener = null @@ -584,64 +576,75 @@ class GutenbergView : WebView { } companion object { - private val warmupScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private const val ASSET_LOADING_TIMEOUT_MS = 5000L + + // Warmup state management + private var warmupHandler: Handler? = null + private var warmupRunnable: Runnable? = null + private var warmupWebView: GutenbergView? = null /** - * Warmup the editor by preloading manifest + * Warmup the editor by preloading assets in a temporary WebView. + * This pre-caches assets to improve editor launch speed. */ @JvmStatic fun warmup(context: Context, configuration: EditorConfiguration) { - if (configuration.enableAssetCaching) { - val library = EditorAssetsLibrary(context, configuration) - // Preload manifest in background - warmupScope.launch { - try { - library.manifestContentForEditor() - } catch (e: Exception) { - Log.e("GutenbergView", "Failed to warmup manifest", e) - } - } + // Cancel any existing warmup + cancelWarmup() + + // Create dedicated warmup WebView + val webView = GutenbergView(context) + webView.initializeWebView() + webView.start(configuration) + warmupWebView = webView + + // Schedule cleanup after assets are loaded + warmupHandler = Handler(Looper.getMainLooper()) + warmupRunnable = Runnable { + cleanupWarmup() } + warmupHandler?.postDelayed(warmupRunnable!!, ASSET_LOADING_TIMEOUT_MS) } /** - * Cancel any ongoing warmup operations + * Cancel any pending warmup and clean up resources. */ @JvmStatic fun cancelWarmup() { - warmupScope.coroutineContext[Job]?.cancelChildren() + warmupRunnable?.let { runnable -> + warmupHandler?.removeCallbacks(runnable) + } + cleanupWarmup() } - } -} - -object GutenbergWebViewPool { - private var preloadedWebView: GutenbergView? = null - @JvmStatic - fun getPreloadedWebView(context: Context): GutenbergView { - val currentView = preloadedWebView - if (currentView == null) { - preloadedWebView = createAndPreloadWebView(context) - } else { - (currentView.parent as? android.view.ViewGroup)?.removeView(currentView) + /** + * Clean up warmup resources. + */ + private fun cleanupWarmup() { + warmupWebView?.let { webView -> + webView.stopLoading() + webView.clearConfig() + webView.destroy() + } + warmupWebView = null + warmupHandler = null + warmupRunnable = null } - return preloadedWebView!! - } - - private fun createAndPreloadWebView(context: Context): GutenbergView { - val webView = GutenbergView(context) - webView.initializeWebView() - webView.loadUrl(ASSET_URL) - return webView - } - @JvmStatic - fun recycleWebView(webView: GutenbergView) { - webView.stopLoading() - webView.clearConfig() - webView.removeAllViews() - webView.destroy() - preloadedWebView = null + /** + * Create a new GutenbergView for the editor. + * Cancels any pending warmup to free resources. + */ + @JvmStatic + fun createForEditor(context: Context): GutenbergView { + // Cancel any pending warmup to free resources + cancelWarmup() + + // Create fresh WebView for editor + val webView = GutenbergView(context) + webView.initializeWebView() + return webView + } } } diff --git a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergWebViewPoolTest.kt b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergWebViewPoolTest.kt deleted file mode 100644 index 38dd27c3..00000000 --- a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergWebViewPoolTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package org.wordpress.gutenberg - -import android.content.Context -import android.view.ViewGroup -import android.widget.FrameLayout -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment - -@RunWith(RobolectricTestRunner::class) -class GutenbergWebViewPoolTest { - - @Mock - private lateinit var mockContext: Context - - private lateinit var context: Context - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - context = RuntimeEnvironment.getApplication() - } - - @Test - fun `getPreloadedWebView should create new webview when none exists`() { - // Clear any existing preloaded webview - GutenbergWebViewPool.recycleWebView(GutenbergWebViewPool.getPreloadedWebView(context)) - - val webView = GutenbergWebViewPool.getPreloadedWebView(context) - assertNotNull(webView) - assertNull(webView.parent) - } - - @Test - fun `getPreloadedWebView should remove parent when webview has parent`() { - // Get a webview and add it to a parent - val webView = GutenbergWebViewPool.getPreloadedWebView(context) - val parent = FrameLayout(context) - parent.addView(webView) - - // Verify it has a parent - assertEquals(parent, webView.parent) - - // Get the webview again - this should remove it from parent - val reusedWebView = GutenbergWebViewPool.getPreloadedWebView(context) - - // Verify it's the same instance but no longer has a parent - assertEquals(webView, reusedWebView) - assertNull(reusedWebView.parent) - } - - @Test - fun `getPreloadedWebView should return same instance when no parent`() { - val webView1 = GutenbergWebViewPool.getPreloadedWebView(context) - val webView2 = GutenbergWebViewPool.getPreloadedWebView(context) - - assertEquals(webView1, webView2) - } - - @Test - fun `recycleWebView should clear the pool`() { - val webView = GutenbergWebViewPool.getPreloadedWebView(context) - GutenbergWebViewPool.recycleWebView(webView) - - // Getting a new webview should create a fresh instance - val newWebView = GutenbergWebViewPool.getPreloadedWebView(context) - assertNotNull(newWebView) - // They should be different instances since the pool was cleared - // Note: This test might be flaky due to object reuse, but it tests the intent - } -}