Skip to content

fix(setup): warm ML Kit barcode scanner at startup (v0.2.4.6-alpha) - #53

Merged
robster7674 merged 1 commit into
glucodroidfrom
fix/mlkit-barcode-warmup
Jun 15, 2026
Merged

fix(setup): warm ML Kit barcode scanner at startup (v0.2.4.6-alpha)#53
robster7674 merged 1 commit into
glucodroidfrom
fix/mlkit-barcode-warmup

Conversation

@robster7674

Copy link
Copy Markdown
Owner

Summary

Fixes a startup crash when adding an i3 (Sinocare iCan) sensor — InlineQrScannerCard could NPE on a null zzi field of ML Kit's internal zzg static holder when BarcodeScanning.getClient(options) raced with the Compose measure pass on cold start.

Changes

  • MainActivity.onCreate — eagerly warm BarcodeScanning on the main thread via reflection (flavor-safe: skips if mlkit not on classpath). Removes the cold-init race for all QR-based sensor wizards (iCanHealth, Dexcom, AccuChek, CareSensAir, Anytime, MQ, Sibionics).
  • InlineQrScannerCard — move BarcodeScanning.getClient(options) out of remember { } into a LaunchedEffect(Unit) wrapped in runCatching. On failure, surfaces the error via the existing scannerError state; the manual-entry button still works.
  • Common/build.gradle — bump versionName 0.2.4.5 → 0.2.4.6, versionCode 8245 → 8246.
  • CLAUDE.md — replace old commit-and-push-immediately workflow with feature-branch + PR workflow.

Root cause (from trace deobfuscation)

Deobfuscated stack: x28.c(o30) = BarcodeScanning.getClient(BarcodeScannerOptions)zzg.zzb(opts) reading zzg.zza (the zzi field). The NPE on the zzi field is the well-known standalone-MLKit static-init race when the call happens during View.measure.

Test plan

  • Cold-start the app, open Add sensor → iCanHealth i3, observe the scan step renders without crash.
  • adb logcat | grep MainActivity should show ML Kit barcode scanner warmed up from the activity PID.
  • QR scan should succeed on the first frame after the camera preview appears.
  • Manual-entry fallback should still work even if ML Kit is unavailable on the device.

Build

./gradlew :Common:assembleMobileLibre3SiDexNogoogleRelease -Pno_x86 -Pno_x86_64 — nogoogle flavor, suitable for F-Droid / sideload.

…ut of measure pass

- MainActivity.onCreate: eagerly warm BarcodeScanning static holder once
  on the main thread via reflection. Eliminates the cold-process race
  where the static <clinit> could be entered from a Compose measure
  pass and NPE on the lib's internal zzg.zza (zzi) field.
- InlineQrScannerCard: move BarcodeScanning.getClient(options) out of
  remember{} into a LaunchedEffect(Unit) wrapped in runCatching. If the
  call still fails for any reason, surface it via the existing
  scannerError state so the manual-entry button remains usable.
- Bump versionName 0.2.4.5 -> 0.2.4.6, versionCode 8245 -> 8246.
- Update CLAUDE.md git-workflow section to use feature branches + PR
  (replaces the old 'commit and push immediately' rule).

Fixes the startup crash when adding an i3 (Sinocare iCan) sensor.
@robster7674
robster7674 merged commit db39af4 into glucodroid Jun 15, 2026
8 checks passed
@robster7674
robster7674 deleted the fix/mlkit-barcode-warmup branch June 15, 2026 06:18
@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR addresses a cold-start NPE crash in InlineQrScannerCard caused by ML Kit's static initializer racing with the Compose measure pass, and bumps the version to 0.2.4.6-alpha.

  • InlineQrScannerCard.kt: BarcodeScanning.getClient() is moved from remember {} into a LaunchedEffect(Unit) wrapped in runCatching, with the scanner stored as nullable state. This is the effective fix for the NPE race.
  • MainActivity.java: Adds a reflective warmUpBarcodeScanner() call in onCreate; however, it always throws NoSuchMethodException because it looks up setBarcodeFormats(int[]) while the actual ML Kit signature is setBarcodeFormats(int, int[]) — the warm-up never completes.
  • CLAUDE.md / build.gradle: Workflow documentation rewrite and routine version bump.

Confidence Score: 3/5

The LaunchedEffect change in InlineQrScannerCard is a real and correct fix for the cold-start crash, but the reflective warm-up in MainActivity never executes, so a key part of the stated defense strategy does not work.

The warm-up code looks up setBarcodeFormats with the single-parameter type int[].class, but the actual ML Kit method requires two parameters (int, int[]). This mismatch causes a NoSuchMethodException on every launch for ML Kit flavors, silently caught and logged as a failure. The logcat sentinel from the test plan will never appear. The crash is still largely mitigated by the LaunchedEffect deferral, but the PR ships broken warm-up code that should be corrected before merging.

Common/src/main/java/tk/glucodata/MainActivity.java — the warmUpBarcodeScanner method needs the correct two-argument reflection signature for setBarcodeFormats.

Important Files Changed

Filename Overview
Common/src/main/java/tk/glucodata/MainActivity.java Adds warmUpBarcodeScanner() in onCreate to eagerly initialise ML Kit; the reflective getMethod("setBarcodeFormats", int[].class) call always throws NoSuchMethodException because the real API is setBarcodeFormats(int, int[]), so the warm-up silently logs a failure on every launch for ML Kit flavors.
Common/src/mobile/java/tk/glucodata/ui/setup/InlineQrScannerCard.kt Moves BarcodeScanning.getClient from remember {} to LaunchedEffect(Unit) with runCatching, preventing the NPE race during Compose measure; correctly nullifies the scanner reference before use and on disposal, but the camera-binding DisposableEffect can clear a scannerError set by the LaunchedEffect when keys change.
Common/build.gradle Routine version bump: versionName 0.2.4.5 to 0.2.4.6, versionCode 8245 to 8246.
CLAUDE.md Replaces the old commit-and-push workflow with a feature-branch PR squash-merge flow and adds a detailed release checklist; no code behaviour changes.

Sequence Diagram

sequenceDiagram
    participant MA as MainActivity.onCreate
    participant WU as warmUpBarcodeScanner()
    participant CE as Compose Engine
    participant LE as LaunchedEffect(Unit)
    participant BS as BarcodeScanning (ML Kit)
    participant AE as analyzerExecutor

    MA->>WU: call (main thread)
    WU->>WU: getMethod("setBarcodeFormats", int[].class)
    WU-->>MA: NoSuchMethodException caught, logs warm-up failed

    MA->>CE: initComposeUI()
    CE->>CE: measure / layout pass
    note over CE: barcodeScanner = null during measure, no NPE

    CE->>LE: launch coroutine (main thread, post-measure)
    LE->>BS: BarcodeScanning.getClient(scannerOptions) in runCatching
    BS-->>LE: BarcodeScanner instance or failure sets scannerError
    LE->>CE: "barcodeScanner = instance, state update triggers recompose"

    CE->>AE: ImageAnalysis.setAnalyzer
    AE->>AE: "val scanner = barcodeScanner, null-check"
    AE->>BS: scanner.process(inputImage)
    BS-->>AE: Task with List of Barcode
    AE->>CE: onScanResult(rawValue) via mainExecutor
Loading

Comments Outside Diff (1)

  1. Common/src/mobile/java/tk/glucodata/ui/setup/InlineQrScannerCard.kt, line 203-217 (link)

    P2 Camera-binding DisposableEffect clears scannerError set by the LaunchedEffect

    When the LaunchedEffect(Unit) fails to initialize the scanner and sets scannerError, the composable re-renders and the camera-binding DisposableEffect eventually re-executes (e.g. when previewView becomes non-null on the first frame). Inside its else branch, scannerError is reset to null, silently clearing the error before the user sees it. Subsequent frames skip analysis because scanner == null, but there is no longer any error indicator — the UI shows a live camera preview with scanning silently doing nothing. The manual-entry button remains available, but the user has no cue to use it.

Reviews (1): Last reviewed commit: "fix(setup): warm ML Kit barcode scanner ..." | Re-trigger Greptile

Comment on lines +569 to +576
optionsBuilderCls.getMethod("setBarcodeFormats", int[].class).invoke(
optionsBuilderCls.getConstructor().newInstance(),
new Object[]{ new int[]{
barcodeCls.getField("FORMAT_QR_CODE").getInt(null),
barcodeCls.getField("FORMAT_DATA_MATRIX").getInt(null)
} }
)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reflection warm-up always fails — wrong method signature

getMethod("setBarcodeFormats", int[].class) looks for the single-parameter overload setBarcodeFormats(int[]), which does not exist. The actual ML Kit API is setBarcodeFormats(int format, int... moreFormats), which compiles to the two-parameter bytecode signature setBarcodeFormats(int, int[]). The result is a NoSuchMethodException on every start for ML Kit-enabled flavors, silently caught by the outer Throwable block, so the log will always show "ML Kit barcode scanner warm-up failed" — never "ML Kit barcode scanner warmed up". The warm-up never actually executes.

The correct reflective call needs two parameter types — int.class for the mandatory first format and int[].class for the varargs remainder — and the invoke call must supply them as two separate arguments rather than wrapping both in a single int[].

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant