diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b975d1e..bc92932 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -34,6 +34,10 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") + // Flutter turns R8 on for release. That strips ML Kit classes + // discovered via manifest metadata / JNI, so OCR fails in the APK. + isMinifyEnabled = false + isShrinkResources = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index a7f5032..350ceb5 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -1,10 +1,15 @@ -# ML Kit text recognition ships Latin only; the plugin still references -# optional script packs that are not in this app. --dontwarn com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions$Builder --dontwarn com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions --dontwarn com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions$Builder --dontwarn com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions --dontwarn com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions$Builder --dontwarn com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions --dontwarn com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions$Builder --dontwarn com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions +# ML Kit text recognition is wired up through manifest metadata, reflection, +# and JNI. R8 otherwise strips those classes and OCR fails in release APKs. +-keep class com.google.mlkit.** { *; } +-keep class com.google.android.gms.internal.mlkit_** { *; } +-keep class com.google.android.gms.internal.mlkit_vision_** { *; } +-keep class com.google.android.libraries.vision.** { *; } +-keep class com.google_mlkit_commons.** { *; } +-keep class com.google_mlkit_text_recognition.** { *; } +-keep class * implements com.google.firebase.components.ComponentRegistrar { *; } + +# The Flutter plugin references optional script packs that this app does not ship. +-dontwarn com.google.mlkit.vision.text.chinese.** +-dontwarn com.google.mlkit.vision.text.devanagari.** +-dontwarn com.google.mlkit.vision.text.japanese.** +-dontwarn com.google.mlkit.vision.text.korean.** diff --git a/android/gradle.properties b/android/gradle.properties index e96108c..495ae06 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +# Flutter enables R8 for release by default. That breaks on-device ML Kit OCR. +shrink=false # This newDsl flag was added by the Flutter template android.newDsl=false # This builtInKotlin flag was added by the Flutter template diff --git a/lib/scan_screen.dart b/lib/scan_screen.dart index 1eaf0aa..05493f3 100644 --- a/lib/scan_screen.dart +++ b/lib/scan_screen.dart @@ -103,10 +103,17 @@ class _ScanScreenState extends State { _error = 'No text found in this image.'; } }); - } catch (error) { + } on PlatformException catch (error) { + debugPrint('OCR failed: ${error.code} ${error.message}'); if (!mounted) return; setState(() { - _error = 'Could not read text from this image.'; + _error = _ocrFailureMessage(error.message); + }); + } catch (error) { + debugPrint('OCR failed: $error'); + if (!mounted) return; + setState(() { + _error = _ocrFailureMessage(error.toString()); }); } finally { await recognizer.close(); @@ -120,6 +127,12 @@ class _ScanScreenState extends State { } } + String _ocrFailureMessage(String? detail) { + const fallback = 'Could not read text from this image.'; + if (detail == null || detail.trim().isEmpty) return fallback; + return '$fallback\n$detail'; + } + Future _openReview(String rawText) async { final items = ReceiptParser.parse(rawText); if (!mounted) return;