Skip to content

Replace Google ML Kit with ZXing#248

Merged
kwsantiago merged 5 commits intomainfrom
Replace-Google-ML-Kit
Apr 19, 2026
Merged

Replace Google ML Kit with ZXing#248
kwsantiago merged 5 commits intomainfrom
Replace-Google-ML-Kit

Conversation

@wksantiago
Copy link
Copy Markdown
Contributor

@wksantiago wksantiago commented Apr 18, 2026

Replaces Google ML Kit barcode scanner with ZXing for F-Droid eligibility. Hardens scanner thread safety and buffer bounds.

Summary by CodeRabbit

  • Refactor

    • Improved QR code scanning implementation for faster, more reliable imports.
  • Chores

    • Removed an unused barcode-scanning dependency.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

Walkthrough

The PR removes the ML Kit barcode-scanning dependency and replaces ML Kit-based async barcode processing in ImportShareScreen.kt with ZXing synchronous QR decoding, manual luminance extraction and rotation handling, updated analyzer control flow, and improved analyzer/executor cleanup.

Changes

Cohort / File(s) Summary
Dependency Management
app/build.gradle.kts
Removed com.google.mlkit:barcode-scanning:17.3.0 implementation dependency.
QR Scanning Implementation
app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt
Replaced ML Kit async barcode pipeline with ZXing MultiFormatReader synchronous decoding. Added LuminanceBuffers, manual plane luminance extraction, rotation handling (90/180/270°), try/finally imageProxy close, inline decode + frameCollector.processQrContent, one-shot scanned gate, and analyzer/executor shutdown and termination handling.

Sequence Diagram(s)

sequenceDiagram
  participant Camera as Camera (CameraX)
  participant Analyzer as Analyzer (ImageProxy)
  participant ZXing as ZXing MultiFormatReader
  participant Collector as FrameCollector
  participant Main as MainExecutor / UI

  Camera->>Analyzer: deliver frame (ImageProxy)
  Analyzer->>Analyzer: extract luminance planes (LuminanceBuffers)
  Analyzer->>Analyzer: rotate/copy luminance if needed
  Analyzer->>ZXing: decode luminance -> try decode QR
  ZXing-->>Analyzer: result (success/failure)
  alt success & validator + scanned gate
    Analyzer->>Collector: processQrContent(...)
    Analyzer->>Main: post onCodeScanned / update UI
  end
  Analyzer->>Camera: close imageProxy (finally)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 Hopped in the dark, found pixels to weigh,
Luminance lined up, we turned night to day.
ML Kit hopped out, ZXing hopped in,
Frames twirl and decode—let the scanning begin! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Replace Google ML Kit with ZXing' directly and accurately summarizes the main change in the changeset, matching the primary objective stated in the PR description.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Replace-Google-ML-Kit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wksantiago
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@wksantiago wksantiago requested a review from kwsantiago April 18, 2026 12:37
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt (1)

100-147: Per-frame allocations in the hot path.

rotateLuminance plus the ByteArray(expected) copy in decodeQrFromImageProxy allocate two fresh byte arrays sized rowStride*height and width*height on every analyzer frame. With STRATEGY_KEEP_ONLY_LATEST that's still steady allocation at camera cadence and creates noticeable GC pressure on low-end devices. Since the analyzer executor is single-threaded, a pair of reusable per-scanner buffers (reallocated only when dimensions change) would be safe and much cheaper. Not a correctness issue — feel free to defer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt` around lines 100 -
147, rotateLuminance and decodeQrFromImageProxy currently allocate fresh
ByteArray buffers on every analyzer frame causing GC pressure; change them to
reuse two per-scanner buffers (one sized rowStride*height for the source/copy
and one sized width*height for rotated output) kept as class-level properties
(e.g., on ImportShareScreen or the scanner instance used by the single-threaded
analyzer) and only reallocate when width/height/rowStride change; update
rotateLuminance to write into the reused output buffer instead of creating a new
ByteArray and make decodeQrFromImageProxy use the reusable source buffer for the
rowStride copy, ensuring synchronization is unnecessary because the analyzer
executor is single-threaded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt`:
- Around line 591-599: The cleanupResources() routine risks calling
reader.reset() while a decodeWithState is still running because
executor.shutdownNow() + awaitTermination(100 ms) doesn't guarantee the analyzer
thread has stopped; update cleanupResources() to only call reader.reset() after
confirming the executor terminated successfully (e.g., check awaitTermination
returned true) or otherwise synchronize access to the MultiFormatReader used by
decodeWithState and cleanupResources (e.g., a shared lock around reader.reset()
and decodeWithState) so that reader.reset() cannot run concurrently with decode
operations; reference the methods/fields cleanupResources, executor.shutdownNow,
executor.awaitTermination, reader.reset and decodeWithState when making the
change.
- Around line 56-66: The Y-plane buffer-size check uses rowStride * height which
rejects valid frames when rowStride > width; update the logic in the image
handling where you access imageProxy.planes.firstOrNull() / plane.buffer to
allocate data of size width * height and copy per-row into that array: for each
row read min(width, rowStride) bytes from buffer accounting for
plane.pixelStride (guarding for pixelStride != 1 by reading pixels
individually), rather than expecting contiguous rowStride*height bytes; keep
buffer.rewind() and ensure you don't return null just because buffer.remaining()
< rowStride * height but instead validate buffer has at least
rowStride*(height-1)+width or perform safe per-row reads.

---

Nitpick comments:
In `@app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt`:
- Around line 100-147: rotateLuminance and decodeQrFromImageProxy currently
allocate fresh ByteArray buffers on every analyzer frame causing GC pressure;
change them to reuse two per-scanner buffers (one sized rowStride*height for the
source/copy and one sized width*height for rotated output) kept as class-level
properties (e.g., on ImportShareScreen or the scanner instance used by the
single-threaded analyzer) and only reallocate when width/height/rowStride
change; update rotateLuminance to write into the reused output buffer instead of
creating a new ByteArray and make decodeQrFromImageProxy use the reusable source
buffer for the rowStride copy, ensuring synchronization is unnecessary because
the analyzer executor is single-threaded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511449ad-5fa0-4282-ac4f-78b2257b4a62

📥 Commits

Reviewing files that changed from the base of the PR and between 013c6e8 and d69b8b8.

📒 Files selected for processing (2)
  • app/build.gradle.kts
  • app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt
💤 Files with no reviewable changes (1)
  • app/build.gradle.kts

Comment thread app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt Outdated
Comment thread app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt (1)

72-72: Consider adding a defensive pixelStride check.

While YUV_420_888 Y plane should always have pixelStride == 1 per the Android specification, adding a defensive check would guard against unexpected device behavior.

🛡️ Optional defensive check
 val plane = imageProxy.planes.firstOrNull() ?: return null
+if (plane.pixelStride != 1) return null
 val buffer = plane.buffer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt` at line 72, The
code currently assumes the Y plane's pixelStride is 1 when doing val plane =
imageProxy.planes.firstOrNull() ?: return null; add a defensive check on
plane.pixelStride (e.g., if (plane.pixelStride != 1) ...) and handle that case
gracefully—either convert by copying/scaling the buffer into a contiguous byte
array or return null/skip processing—so that the routine that reads the Y buffer
(in ImportShareScreen's image-to-byte/bitmap logic using imageProxy and plane)
does not misinterpret strided data on devices that violate the expectation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt`:
- Line 72: The code currently assumes the Y plane's pixelStride is 1 when doing
val plane = imageProxy.planes.firstOrNull() ?: return null; add a defensive
check on plane.pixelStride (e.g., if (plane.pixelStride != 1) ...) and handle
that case gracefully—either convert by copying/scaling the buffer into a
contiguous byte array or return null/skip processing—so that the routine that
reads the Y buffer (in ImportShareScreen's image-to-byte/bitmap logic using
imageProxy and plane) does not misinterpret strided data on devices that violate
the expectation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56d7d1c7-a56b-499c-a63f-7bfba02ec863

📥 Commits

Reviewing files that changed from the base of the PR and between d69b8b8 and ed6cb4f.

📒 Files selected for processing (1)
  • app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt

Copy link
Copy Markdown
Contributor

@kwsantiago kwsantiago left a comment

Choose a reason for hiding this comment

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

ACK ed6cb4f

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.

Replace Google ML Kit barcode scanner with open-source alternative for F-Droid eligibility

2 participants