Skip to content

fix(document_scanner): prevent 'Reply already submitted' on duplicate activity result#858

Closed
MilanJa wants to merge 1 commit into
flutter-ml:developfrom
MilanJa:fix/document-scanner-reply-already-submitted
Closed

fix(document_scanner): prevent 'Reply already submitted' on duplicate activity result#858
MilanJa wants to merge 1 commit into
flutter-ml:developfrom
MilanJa:fix/document-scanner-reply-already-submitted

Conversation

@MilanJa
Copy link
Copy Markdown

@MilanJa MilanJa commented Apr 16, 2026

Summary

Fixes IllegalStateException: Reply already submitted thrown from DocumentScanner.handleScanningResult (and the cancel/error paths) when the platform delivers onActivityResult more than once for the same START_DOCUMENT_ACTIVITY request code.

Fixes #857.

Root cause

DocumentScanner stores the caller's MethodChannel.Result in pendingResult but never clears the field after completing it:

// before
pendingResult?.success(resultMap)            // in handleScanningResult
pendingResult?.error(TAG, "Operation cancelled", null)  // in onActivityResult
// ...pendingResult still non-null, pointing at an already-completed reply

If the OS then redelivers the activity result — which ColorOS (Oppo) is observed to do across certain lifecycle transitions — the second onActivityResult invocation calls .success(...) / .error(...) on the already-completed MethodChannel.Result, and DartMessenger$Reply.reply throws IllegalStateException: Reply already submitted, crashing the host app.

Stack trace (abridged) from the linked issue:

Caused by java.lang.IllegalStateException: Reply already submitted
    at io.flutter.embedding.engine.dart.DartMessenger$Reply.reply(DartMessenger.java:425)
    at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler$1.success(MethodChannel.java:272)
    at com.google_mlkit_document_scanner.DocumentScanner.handleScanningResult(DocumentScanner.kt:...)
    at com.google_mlkit_document_scanner.DocumentScanner.onActivityResult(DocumentScanner.kt:...)
    at io.flutter.embedding.engine.FlutterEngineConnectionRegistry$FlutterEngineActivityPluginBinding.onActivityResult(...)

Fix

Introduce a consumePendingResult() helper that atomically returns the current pendingResult and nulls the field, and route every submission site through it:

  • handleScanningResult (success path)
  • onActivityResult RESULT_CANCELED / unknown result branches
  • handleScanner error paths (invalid options, intent-sender failures)

A redelivered activity result now short-circuits via the null receiver (consumePendingResult()?.xxx(...)) instead of double-submitting.

private fun consumePendingResult(): MethodChannel.Result? {
    val reply = pendingResult
    pendingResult = null
    return reply
}

The change is confined to one file: packages/google_mlkit_document_scanner/android/src/main/kotlin/com/google_mlkit_document_scanner/DocumentScanner.kt (+20/-6).

Validation

  • flutter build apk --debug on packages/example compiles cleanly against the patched Kotlin source.
  • The user-facing crash cannot be reproduced on demand (it is OEM-specific timing triggered by ColorOS redelivery), but the mechanism is eliminated: after the first .success() / .error(), pendingResult is null and any subsequent callback is a no-op.

Suggested CHANGELOG entry (for maintainers)

## 0.4.2

* Fix `IllegalStateException: Reply already submitted` crash when `onActivityResult` is
  delivered more than once (observed on Oppo/ColorOS devices).

I did not bump the package version in this PR — leaving that to the maintainer release cadence.

Test plan

  • flutter build apk --debug on packages/example — passes.
  • Happy path smoke test on an Android device (maintainers).
  • Cancel path smoke test (maintainers).

… activity result

`DocumentScanner` stored `MethodChannel.Result` in `pendingResult` and
invoked `.success(...)` / `.error(...)` on it without ever clearing the
field. When the platform delivers `onActivityResult` more than once for
`START_DOCUMENT_ACTIVITY` (observed on Oppo/ColorOS devices via Firebase
Crashlytics), the second invocation re-submits on the already-completed
reply and throws `IllegalStateException: Reply already submitted`,
crashing the host app.

Introduce `consumePendingResult()` which atomically returns the current
`pendingResult` and nulls the field, and route every submission path
(success, cancelled, unknown, invalid-options, intent-sender failures)
through it. A redelivered activity result now short-circuits via the
null receiver instead of crashing.

Fixes flutter-ml#857
@github-actions
Copy link
Copy Markdown

This PR is stale because it has been open for 14 days with no activity.

@github-actions github-actions Bot added the stale label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

This PR was closed because it has been inactive for 14 days since being marked as stale.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IllegalStateException: Reply already submitted in DocumentScanner.handleScanningResult (Oppo devices)

1 participant