#982

Fix QR scanner crash in release builds and add paste fallback

OpentranKILLwants to merge intofrom

Summary

Opening Verify → Scan crashes every minified release build (2.0.x, any device). Debug builds aren't minified, so they never show it.

Fixes #878, fixes #970.

Root cause, from the retraced crash and the startup logcat of a release build:

QRCodeAnalyzer.<init>                      (VerificationSheet.kt)
  BarcodeScanning.getClient()              → NullPointerException: ML Kit was never initialized
     because at startup:
     MlKitInitProvider.onCreate
       ComponentDiscovery: Could not instantiate CommonComponentRegistrar,
                           VisionCommonRegistrar, BarcodeRegistrar
         NoSuchMethodException: <init> []  ← R8 full mode removed the no-arg constructors

The fix keeps those constructors. It follows the same pattern as the existing Room rule:

 # app/proguard-rules.pro
+-keepclassmembers class * implements com.google.firebase.components.ComponentRegistrar {
+    <init>();
+}

The Scan tab also gets a fallback and feedback:

 <ScanTabContent>
-  <ScannerView onScan>                 # rejected codes did nothing
+  <ScannerView onScan = handleCode>
+  <TextButton "Paste verification URL"> → handleCode(clipboard text)
+  handleCode: success → toast "Verification requested"
+              failure → inline "Invalid or expired code, or this person isn't connected nearby"
  • Paste button: verifies a bitchat://verify link read with another QR app. This helps when the camera is unavailable or camera permission is denied.
  • Rejected camera scans: codes that are invalid, expired, or from a peer who isn't connected used to do nothing. They now show the same inline message. It's inline rather than a toast because Android 12+ cuts toasts to two lines.
  • Strings: the button and success message reuse the existing, already-translated verify_paste_label and verify_scanned. The new verify_scan_failed is added to every locale, in English where that locale's verify strings are still in English.

Evidence

The tests below used a minified release build signed with the debug key, on an Android 16 emulator unless noted otherwise.

  • Before (main):

    • Opening Scan closes the app.
    • Startup logcat shows Could not instantiate …Registrar three times.
    • Removing ImplementationMode.EMBEDDED, as #976 and #974 do, still crashes the same way.

    After:

    • No registrar warnings.
    • The Scan tab shows the camera preview and nothing crashes.
    • The same result on a physical Android 16 phone.
  • Camera path: a physical phone scanning an emulator's QR code shows the inline "not connected nearby" message. That's expected, because an emulator can't join a phone's mesh. It shows the camera and ML Kit decoding work end to end.

  • Successful verification: two emulators meshed over the emulator's virtual Bluetooth.

    • Pasting emulator A's verify link on emulator B shows "Verification requested".
    • B then lists A with the verified badge and the message "Verified ⟨A⟩".
  • Paste with an empty clipboard: shows the inline error and doesn't crash.

  • CI tasks: testDebugUnitTest passes (638 tests, 0 failures). lintDebug reports no new issues in the changed files.

Mesh Lab wasn't run: no transport, routing, protocol or crypto code changed.

Merge Danger

Door: two-way

Blast Radius: small

  • The keep rule only matches classes implementing ComponentRegistrar, which here are ML Kit's three registrars.
  • The UI change is limited to the Scan tab.
  • Related PRs #976, #900 and #899 change the CameraX viewfinder setup but don't address this crash. For #976's change, I checked this on the release build.

🤖 Generated with Claude Code

Activity

4
chatgpt-codex-connector[bot]8d ago

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code Review✅ Completed 2026-10-22T18:12:34.023276Z5160efcPR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

zhu1202127d ago

But I have already successfully tested it on Android 16

callebtcreviewed6d ago
callebtc6d ago

Review: looks good to merge ✅

I confirmed the root cause with an R8 build. I ran minifyReleaseWithR8 with main's proguard-rules.pro and then with this PR's rules.

  • With main's rules, the mapping keeps CommonComponentRegistrar, VisionCommonRegistrar and BarcodeRegistrar but drops their no-arg <init>(). The firebase-components consumer rule (-keep class * implements ComponentRegistrar) keeps the classes but not the constructors in R8 full mode.
  • With this PR's rules, all three keep <init>().

I also checked every other reflection-loaded <meta-data> class in the merged release manifest: the CameraX config provider, WorkManager, the lifecycle, emoji and profile installers, and the okhttp initializer. They all keep their constructors under main's rules, so only these three registrars needed the new rule.

Nits (non-blocking):

  • Paste with an empty clipboard, or scanning a QR code that isn't a bitchat code, shows the "invalid or expired" message. A separate message would be clearer.
  • BarcodeScanning.getClient() still runs unguarded inside remember {}. If ML Kit fails for some other reason, the screen still crashes instead of falling back to paste.
  • About 17 of the 33 locale files get the English text copied in (ar, bn, fa, fil, hi, id, ka, ko, mg, ne, nl, pa-rPK, sv, th, tr, ur, vi). At runtime this is the same as falling back to English, but it hides the string from MissingTranslation lint and translators. Prefer leaving those out. The new values-eu gets no entry.
  • LocalClipboardManager is deprecated (warning only).

No CI has run on this PR yet; it needs workflow approval. It merges cleanly with main. It overlaps #984 in proguard-rules.pro (same keep rule), so only one of them should carry it.