Skip to content

bug(demo-android): Save can hang forever if tapped before editor loads #444

@jkmassel

Description

@jkmassel

Context

In PR #433 (feat/demo-edit-existing-posts), the Android demo app's EditorActivity.persistPost() uses suspendCancellableCoroutine to bridge the callback-based GutenbergView.getTitleAndContent() into a suspend function.

Bug

GutenbergView.getTitleAndContent() has an early-return guard:

if (!isEditorLoaded) {
    Log.e("GutenbergView", "You can't change the editor content until it has loaded")
    return  // callback is NEVER invoked
}

When isEditorLoaded is false, the method returns without calling the callback. In persistPost(), this means cont.resume() is never called and the coroutine suspends indefinitely:

val titleAndContent = suspendCancellableCoroutine<Pair<CharSequence, CharSequence>> { cont ->
    view.getTitleAndContent(
        originalContent = configuration.content,
        callback = object : GutenbergView.TitleAndContentCallback {
            override fun onResult(title: CharSequence, content: CharSequence) {
                if (cont.isActive) cont.resume(title to content)
            }
        }
    )
}

There is no timeout, no invokeOnCancellation, and no fallback.

The canSave guard (!isSaving && accountId != null && configuration.postId != null) does not check editor load state, so the Save button is enabled before onEditorLoaded() fires. The gutenbergViewRef is set as soon as the AndroidView factory runs — before the editor JS finishes loading — so there is a race window where the user can tap Save and trigger this path.

Impact

If triggered, isSaving remains true permanently (it is only reset in the finally block, which can't execute until the coroutine completes). The Save button is disabled for the rest of the session with no error message or way to recover.

Low probability in practice (requires tapping Save before the editor finishes loading), but severe consequence (permanently broken Save requiring app restart).

Possible fixes

Any one of these would address it:

  1. Gate canSave on editor load state — expose an isEditorLoaded signal to the Compose UI and include it in canSave, matching the iOS approach (isEditorReady && !isSaving && hasPostID)
  2. Add a timeout — wrap the suspendCancellableCoroutine in withTimeout()
  3. Always invoke the callback — change getTitleAndContent() in the library to call the callback with an error/empty result even when the editor isn't loaded, rather than silently returning

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions