@@ -24,16 +24,17 @@ import android.webkit.WebView
2424import android.webkit.WebViewClient
2525import androidx.webkit.WebViewAssetLoader
2626import androidx.webkit.WebViewAssetLoader.AssetsPathHandler
27+ import kotlinx.coroutines.CoroutineScope
2728import kotlinx.coroutines.Dispatchers
29+ import kotlinx.coroutines.Job
30+ import kotlinx.coroutines.launch
2831import kotlinx.coroutines.withContext
2932import org.json.JSONException
3033import org.json.JSONObject
3134import org.wordpress.gutenberg.model.EditorConfiguration
3235import org.wordpress.gutenberg.model.EditorDependencies
3336import org.wordpress.gutenberg.model.GBKitGlobal
34- import org.wordpress.gutenberg.stores.EditorAssetsLibrary
35- import java.io.File
36- import java.net.URL
37+ import org.wordpress.gutenberg.services.EditorService
3738import java.util.Locale
3839
3940const val ASSET_URL = " https://appassets.androidplatform.net/assets/index.html"
@@ -63,6 +64,10 @@ class GutenbergView : WebView {
6364 private var autocompleterTriggeredListener: AutocompleterTriggeredListener ? = null
6465 private var modalDialogStateListener: ModalDialogStateListener ? = null
6566 private var networkRequestListener: NetworkRequestListener ? = null
67+ private var loadingListener: EditorLoadingListener ? = null
68+
69+ private val coroutineScope = CoroutineScope (Dispatchers .Main + Job ())
70+ private var dependencyLoadingJob: Job ? = null
6671
6772 /* *
6873 * Stores the contextId from the most recent openMediaLibrary call
@@ -119,6 +124,10 @@ class GutenbergView : WebView {
119124 editorDidBecomeAvailableListener = listener
120125 }
121126
127+ fun setEditorLoadingListener (listener : EditorLoadingListener ? ) {
128+ loadingListener = listener
129+ }
130+
122131 constructor (context: Context ) : super (context)
123132 constructor (context: Context , attrs: AttributeSet ) : super (context, attrs)
124133 constructor (context: Context , attrs: AttributeSet , defStyle: Int ) : super (
@@ -262,25 +271,82 @@ class GutenbergView : WebView {
262271 }
263272 }
264273
274+ /* *
275+ * Starts the editor with the given configuration and optional dependencies.
276+ *
277+ * ## Loading Flows
278+ *
279+ * **When dependencies are provided (Fast Path):**
280+ * The editor loads immediately using the pre-fetched dependencies.
281+ * Only `onDependencyLoadingFinished()` and `onEditorReady()` callbacks are invoked.
282+ *
283+ * **When dependencies are null (Async Flow):**
284+ * Dependencies are fetched asynchronously with progress reporting.
285+ * All loading callbacks are invoked in order:
286+ * 1. `onDependencyLoadingStarted()`
287+ * 2. `onDependencyLoadingProgress()` (multiple times)
288+ * 3. `onDependencyLoadingFinished()`
289+ * 4. `onEditorReady()`
290+ *
291+ * @param configuration The editor configuration.
292+ * @param dependencies Pre-fetched dependencies, or null to fetch them asynchronously.
293+ */
265294 fun start (configuration : EditorConfiguration , dependencies : EditorDependencies ? = null) {
266295 this .configuration = configuration
267- this .dependencies = dependencies
268296
269- // Set up asset caching if enabled
270- if (configuration.enableAssetCaching) {
271- val storageRoot = getCacheDirectory(configuration)
272- val httpClient = EditorHTTPClient (authHeader = configuration.authHeader)
273- val library = EditorAssetsLibrary (
274- configuration = configuration,
275- httpClient = httpClient,
276- storageRoot = storageRoot
277- )
278- val cachedInterceptor = CachedAssetRequestInterceptor (
279- library,
280- configuration.cachedAssetHosts
281- )
282- requestInterceptor = cachedInterceptor
297+ if (dependencies != null ) {
298+ // FAST PATH: Dependencies were provided - load immediately
299+ loadEditor(dependencies)
300+ } else {
301+ // ASYNC FLOW: No dependencies - fetch them asynchronously
302+ prepareAndLoadEditor()
283303 }
304+ }
305+
306+ /* *
307+ * Fetches all required dependencies and then loads the editor.
308+ *
309+ * This method is the entry point for the async flow when no dependencies were provided.
310+ */
311+ private fun prepareAndLoadEditor () {
312+ loadingListener?.onDependencyLoadingStarted()
313+
314+ dependencyLoadingJob = coroutineScope.launch {
315+ try {
316+ val editorService = EditorService .create(
317+ context = context,
318+ configuration = configuration
319+ )
320+
321+ val fetchedDependencies = editorService.prepare { progress ->
322+ loadingListener?.onDependencyLoadingProgress(progress)
323+ }
324+
325+ // Store dependencies and load the editor
326+ loadEditor(fetchedDependencies)
327+ } catch (e: Exception ) {
328+ Log .e(" GutenbergView" , " Failed to load dependencies" , e)
329+ loadingListener?.onDependencyLoadingFailed(e)
330+ }
331+ }
332+ }
333+
334+ /* *
335+ * Loads the editor with the given dependencies.
336+ *
337+ * This is the shared loading path used by both flows after dependencies are available.
338+ */
339+ private fun loadEditor (dependencies : EditorDependencies ) {
340+ this .dependencies = dependencies
341+
342+ // Set up asset caching
343+ requestInterceptor = CachedAssetRequestInterceptor (
344+ dependencies.assetBundle,
345+ configuration.cachedAssetHosts
346+ )
347+
348+ // Notify that dependency loading is complete (spinner phase begins)
349+ loadingListener?.onDependencyLoadingFinished()
284350
285351 initializeWebView()
286352
@@ -292,13 +358,13 @@ class GutenbergView : WebView {
292358 this .clearCache(true )
293359 // All cookies are third-party cookies because the root of this document
294360 // lives under `https://appassets.androidplatform.net`
295- CookieManager .getInstance().setAcceptThirdPartyCookies(this , true );
361+ CookieManager .getInstance().setAcceptThirdPartyCookies(this , true )
296362
297363 // Erase all local cookies before loading the URL – we don't want to persist
298364 // anything between uses – otherwise we might send the wrong cookies
299365 CookieManager .getInstance().removeAllCookies {
300366 CookieManager .getInstance().flush()
301- for (cookie in configuration.cookies) {
367+ for (cookie in configuration.cookies) {
302368 CookieManager .getInstance().setCookie(cookie.key, cookie.value)
303369 }
304370 this .loadUrl(editorUrl)
@@ -468,6 +534,7 @@ class GutenbergView : WebView {
468534 isEditorLoaded = true
469535 handler.post {
470536 if (! didFireEditorLoaded) {
537+ loadingListener?.onEditorReady()
471538 editorDidBecomeAvailableListener?.onEditorAvailable(this )
472539 this .didFireEditorLoaded = true
473540 this .visibility = VISIBLE
@@ -691,14 +758,15 @@ class GutenbergView : WebView {
691758
692759 override fun onDetachedFromWindow () {
693760 super .onDetachedFromWindow()
761+ dependencyLoadingJob?.cancel()
694762 clearConfig()
695763 this .stopLoading()
696- (requestInterceptor as ? CachedAssetRequestInterceptor )?.shutdown()
697764 FileCache .clearCache(context)
698765 contentChangeListener = null
699766 historyChangeListener = null
700767 featuredImageChangeListener = null
701768 editorDidBecomeAvailableListener = null
769+ loadingListener = null
702770 filePathCallback = null
703771 onFileChooserRequested = null
704772 autocompleterTriggeredListener = null
@@ -708,26 +776,6 @@ class GutenbergView : WebView {
708776 this .destroy()
709777 }
710778
711- private fun getCacheDirectory (configuration : EditorConfiguration ): File {
712- var siteName = " shared"
713-
714- if (configuration.siteURL.isNotEmpty()) {
715- try {
716- val url = URL (configuration.siteURL)
717- val host = url.host.replace(" :" , " -" )
718- val path = url.path.replace(" /" , " -" ).trim(' -' )
719- siteName = if (path.isEmpty()) host else " $host -$path "
720-
721- // Remove illegal characters
722- siteName = siteName.replace(Regex (" [/:\\\\ ?%*|\" <>]" ), " -" )
723- } catch (e: Exception ) {
724- Log .w(" GutenbergView" , " Failed to parse site URL for cache directory" , e)
725- }
726- }
727-
728- return File (context.filesDir, " editor-caches/$siteName " )
729- }
730-
731779 companion object {
732780 private const val ASSET_LOADING_TIMEOUT_MS = 5000L
733781
@@ -748,7 +796,7 @@ class GutenbergView : WebView {
748796 // Create dedicated warmup WebView
749797 val webView = GutenbergView (context)
750798 webView.initializeWebView()
751- webView.start(configuration)
799+ webView.start(configuration, EditorDependencies .empty )
752800 warmupWebView = webView
753801
754802 // Schedule cleanup after assets are loaded
0 commit comments