From b5d30c96141386bacdec41f2c0e788f235e3e026 Mon Sep 17 00:00:00 2001 From: Milan Jansen Date: Thu, 16 Apr 2026 10:19:27 +0200 Subject: [PATCH] fix(document_scanner): prevent 'Reply already submitted' on duplicate 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 #857 --- .../DocumentScanner.kt | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/google_mlkit_document_scanner/android/src/main/kotlin/com/google_mlkit_document_scanner/DocumentScanner.kt b/packages/google_mlkit_document_scanner/android/src/main/kotlin/com/google_mlkit_document_scanner/DocumentScanner.kt index f4cc6c8e..632913d1 100644 --- a/packages/google_mlkit_document_scanner/android/src/main/kotlin/com/google_mlkit_document_scanner/DocumentScanner.kt +++ b/packages/google_mlkit_document_scanner/android/src/main/kotlin/com/google_mlkit_document_scanner/DocumentScanner.kt @@ -61,7 +61,7 @@ class DocumentScanner( if (scanner == null) { val options = call.argument>("options") ?: run { - result.error(TAG, "Invalid options", null) + consumePendingResult()?.error(TAG, "Invalid options", null) return } val scannerOptions = parseOptions(options) @@ -76,10 +76,10 @@ class DocumentScanner( try { activity.startIntentSenderForResult(intentSender, START_DOCUMENT_ACTIVITY, null, 0, 0, 0) } catch (e: IntentSender.SendIntentException) { - result.error(TAG, "Failed to start document scanner", null) + consumePendingResult()?.error(TAG, "Failed to start document scanner", null) } }.addOnFailureListener { - result.error(TAG, "Failed to start document scanner", null) + consumePendingResult()?.error(TAG, "Failed to start document scanner", null) } } @@ -141,11 +141,11 @@ class DocumentScanner( } Activity.RESULT_CANCELED -> { - pendingResult?.error(TAG, "Operation cancelled", null) + consumePendingResult()?.error(TAG, "Operation cancelled", null) } else -> { - pendingResult?.error(TAG, "Unknown Error", null) + consumePendingResult()?.error(TAG, "Unknown Error", null) } } return true @@ -154,6 +154,8 @@ class DocumentScanner( } private fun handleScanningResult(result: GmsDocumentScanningResult) { + val reply = consumePendingResult() ?: return + val resultMap = HashMap() val pdf = result.pdf @@ -174,6 +176,18 @@ class DocumentScanner( resultMap["images"] = null } - pendingResult?.success(resultMap) + reply.success(resultMap) + } + + /** + * Atomically returns the current [pendingResult] and clears the field, so a subsequent + * duplicate callback (e.g. a redelivered activity result on some OEM builds) cannot submit + * a second reply on the same [MethodChannel.Result] and trigger + * `IllegalStateException: Reply already submitted`. + */ + private fun consumePendingResult(): MethodChannel.Result? { + val reply = pendingResult + pendingResult = null + return reply } }