Skip to content

chore: update Kscan to 0.6.0#1164

Merged
rodvar merged 5 commits intobisq-network:mainfrom
wodoro:update_kscan
Feb 18, 2026
Merged

chore: update Kscan to 0.6.0#1164
rodvar merged 5 commits intobisq-network:mainfrom
wodoro:update_kscan

Conversation

@wodoro
Copy link
Contributor

@wodoro wodoro commented Feb 16, 2026

There's small differences between our version of Kscan and upstream
I tried to keep our UI changes, while updating the rest and fixed the Ktlint issues

Summary by CodeRabbit

  • New Features

    • New explicit barcode format enum and broader format mappings for more predictable scanning across platforms.
    • Inverted-image retry path to improve scan success on challenging captures.
  • Bug Fixes

    • Improved duplicate-detection and format filtering for more accurate results.
    • More reliable resource cleanup on success/failure/cancel.
  • Improvements

    • Consolidated scanner UI with stabilized torch and zoom controls.
    • Improved iOS camera threading for smoother startup/shutdown.
  • Documentation

    • Updated scanner README source references.
  • Tests

    • Added mapper and raw-bytes unit tests.
  • Chores

    • Bumped Android camera library version.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Consolidates format representations into a new public enum and platform mappers, removes legacy BarcodeFormats objects, refactors scanner UI into ScannerViewContent, updates analyzer/camera flows (including inverted-image re-scan and background session threading), adds iOS utilities and tests, and bumps a Gradle dependency and README references.

Changes

Cohort / File(s) Summary
Deps & Docs
gradle/libs.versions.toml, shared/kscan/README.md
Version bump for androidx-camera; updated upstream commit/release references in README.
Public API: Formats
shared/kscan/src/commonMain/kotlin/org/ncgroup/kscan/BarcodeFormat.kt, shared/kscan/src/commonMain/kotlin/org/ncgroup/kscan/BarcodeFormats.kt, shared/kscan/src/androidMain/kotlin/.../BarcodeFormats.android.kt, shared/kscan/src/iosMain/kotlin/.../BarcodeFormats.ios.kt
Introduced new exported enum BarcodeFormat and removed legacy BarcodeFormats objects/constants across common/android/ios sources.
Platform Mappers
shared/kscan/src/androidMain/kotlin/.../BarcodeFormatMapper.kt, shared/kscan/src/iosMain/kotlin/.../BarcodeFormatMapper.kt
Added platform mappers to convert between BarcodeFormat and ML Kit / AVFoundation types, with helpers (toMlKitFormats/toAvTypes, toAppFormat, isKnownFormat).
Analyzer & Camera
shared/kscan/src/androidMain/kotlin/.../BarcodeAnalyzer.kt, shared/kscan/src/iosMain/kotlin/.../CameraViewController.kt
Android analyzer now uses mapper, filters by requested formats, adds inverted-image re-scan, and changes constructor to accept camera accessor + filter; iOS controller moves session start/stop to background queue, switches to mapper usage, and updates teardown threading.
UI / Composables
shared/kscan/src/commonMain/kotlin/.../ScannerViewContent.kt, shared/kscan/src/androidMain/kotlin/.../ScannerView.android.kt, shared/kscan/src/iosMain/kotlin/.../ScannerView.ios.kt
Added internal ScannerViewContent composable and refactored platform ScannerView implementations to centralize lifecycle, result/cancel/zoom/torch wiring and reuse the content wrapper.
iOS Utils & Tests
shared/kscan/src/iosMain/kotlin/.../RawBytesEncoder.kt, shared/kscan/src/iosTest/kotlin/.../RawBytesEncoderTest.kt, shared/kscan/src/iosTest/kotlin/.../BarcodeFormatMapperTest.kt
Added stringToRawBytes utility and new iOS unit tests for mapper and raw-byte encoder.
App Integration
shared/presentation/src/commonMain/kotlin/.../BarcodeScannerView.kt
Updated default codeTypes to use BarcodeFormat.FORMAT_QR_CODE (new enum) replacing removed constant.

Sequence Diagram(s)

sequenceDiagram
  participant CameraProvider as CameraProvider
  participant ImageAnalysis as ImageAnalysis
  participant Analyzer as BarcodeAnalyzer
  participant ScannerLib as MLKit/AVScanner
  participant UI as ScannerView/ResultHandler

  CameraProvider->>ImageAnalysis: deliver ImageProxy
  ImageAnalysis->>Analyzer: analyze(imageProxy)
  Analyzer->>ScannerLib: process(image)
  ScannerLib-->>Analyzer: barcodes[]
  Analyzer->>Analyzer: filter by requested formats (BarcodeFormatMapper)
  alt matching barcodes & duplicate check
    Analyzer->>UI: emit OnSuccess
    UI->>CameraProvider: unbind/stop
    Analyzer->>ImageAnalysis: close ImageProxy
  else none relevant
    Analyzer->>Analyzer: create inverted NV21-like image
    Analyzer->>ScannerLib: process(inverted image)
    ScannerLib-->>Analyzer: barcodes[]
    alt matching barcodes found
      Analyzer->>UI: emit OnSuccess
      UI->>CameraProvider: unbind/stop
    else still none
      Analyzer->>UI: emit OnFailed / continue
    end
    Analyzer->>ImageAnalysis: close ImageProxy
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through enums and mappers, neat and bright,
I flipped inverted pixels in the silver night,
Background threads now hum, sessions stop with care,
UI and camera shared their tidy new lair,
Old constants burrowed out — the code feels light.

🚥 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 title 'chore: update Kscan to 0.6.0' directly and clearly summarizes the main change: updating the Kscan dependency from 0.4.0 to 0.6.0.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Contributor

@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: 3

🤖 Fix all issues with AI agents
In `@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/BarcodeAnalyzer.kt`:
- Around line 119-156: The Y-plane padding can corrupt the NV21 layout; in
createInvertedInputImage you must copy only the tightly packed Y bytes (width *
height) into nv21Bytes instead of using yBuffer.remaining() (ySize). Read
yPlane.rowStride and copy row-by-row: for each row read width bytes from yBuffer
at the proper row offset into nv21Bytes (taking into account rowStride), apply
the inversion per byte while writing into the target buffer, and stop after
width*height bytes; then fill the UV section starting at offset width*height
with 127. Update references: createInvertedInputImage, yPlane, yBuffer, yBytes,
nv21Bytes, and replace the single bulk System.arraycopy with a row-wise copy
that strips padding.

In
`@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`:
- Around line 91-95: LaunchedEffect is keyed on both initializationError and the
function parameter result, so if result is a new lambda on recomposition the
effect will restart and repeatedly call result(BarcodeResult.OnFailed(...)); fix
by keying the LaunchedEffect only on initializationError (use
LaunchedEffect(initializationError)) or keep result stable via
rememberUpdatedState(result) and use the remembered value inside the effect;
update the ScannerView.android.kt usage around LaunchedEffect,
initializationError, and result accordingly.
- Around line 138-151: The BarcodeAnalyzer is capturing the outer nullable var
camera while it's still null (passed at imageAnalysis.setAnalyzer), so later
updates from bindCamera() don't reach it; change the flow to ensure a non-null
Camera is provided after bindCamera() — either call imageAnalysis.setAnalyzer
only after bindCamera() returns the Camera (i.e., move the setAnalyzer into the
success path of bindCamera()), or modify BarcodeAnalyzer to accept a
supplier/lambda (e.g., () -> Camera?) instead of Camera so it defers reading the
outer camera; update usages in scannerOptions within BarcodeAnalyzer accordingly
to call the supplier when needed.
🧹 Nitpick comments (6)
shared/kscan/src/commonMain/kotlin/org/ncgroup/kscan/BarcodeFormat.kt (1)

29-57: Mixing barcode formats and value types in a single enum.

The enum combines two distinct concepts — physical encoding formats (FORMAT_*) and data content types (TYPE_*). While this mirrors the upstream KScan design and the KDoc clearly documents both categories, consider whether a future split into two enums would improve type safety (e.g., preventing a TYPE_* value from being passed where only a scan format is expected). Not blocking since this aligns with upstream.

shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/BarcodeAnalyzer.kt (1)

100-116: Double imageProxy.close() on failure/cancel in inverted scan path.

addOnFailureListener (Line 109) and addOnCanceledListener (Line 112) each close imageProxy, but addOnCompleteListener (Line 115) also closes it unconditionally. While ImageProxy.close() is idempotent in CameraX, the intent is clearer if only addOnCompleteListener handles cleanup:

♻️ Suggested cleanup
         scanner
             .process(invertedImage)
             .addOnSuccessListener { barcodes ->
                 val relevantBarcodes = barcodes.filter { isRequestedFormat(it) }
                 if (relevantBarcodes.isNotEmpty()) {
                     processFoundBarcodes(relevantBarcodes)
                 }
             }.addOnFailureListener {
                 onFailed(it)
-                imageProxy.close()
             }.addOnCanceledListener {
                 onCanceled()
-                imageProxy.close()
             }.addOnCompleteListener {
                 // CRITICAL: Always close the proxy after the final attempt
                 imageProxy.close()
             }
shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt (1)

166-178: Redundant cleanup: both onRelease and DisposableEffect call unbindAll().

AndroidView.onRelease (Line 167) calls provider.unbindAll(), and DisposableEffect (Line 174) also calls cameraProvider?.unbindAll(). While double-unbinding is likely safe (idempotent), the duplication is unnecessary. Consider consolidating cleanup in one place.

shared/kscan/src/iosMain/kotlin/org/ncgroup/kscan/BarcodeFormatMapper.kt (1)

38-43: Behavioral asymmetry with Android: empty mapping result returns empty list instead of falling back.

On Android, if no app formats map to valid ML Kit formats, toMlKitFormats falls back to FORMAT_ALL_FORMATS. Here on iOS, toAvTypes returns an empty list, which is then caught downstream in CameraViewController.setupMetadataOutput as an error. The behavior is safe (the error is reported), but the inconsistency means the same invalid input produces a scan-all-formats fallback on Android vs. an error on iOS.

If this is intentional, consider adding a brief comment explaining the design choice.

shared/kscan/src/iosMain/kotlin/org/ncgroup/kscan/CameraViewController.kt (2)

97-99: Good improvement: moving startRunning()/stopRunning() off the main thread.

Apple's documentation states that AVCaptureSession.startRunning() and stopRunning() are blocking calls that should not be invoked on the main thread. Dispatching them to a global queue is the correct approach.

The pattern dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(), 0u) is repeated 5 times. Consider extracting it to a private property for readability.

Optional: extract background queue
 class CameraViewController(
     ...
 ) : UIViewController(null, null),
     AVCaptureMetadataOutputObjectsDelegateProtocol {
+    private val backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(), 0u)
+
     private lateinit var captureSession: AVCaptureSession

Then replace all occurrences with backgroundQueue.

Also applies to: 124-128, 133-137, 188-192, 247-260


243-243: toFormat() extension is only used once — consider inlining.

The private extension AVMetadataObjectType.toFormat() (Line 243) wraps a single BarcodeFormatMapper.toAppFormat(this) call and is used only at Line 176. It adds indirection without much readability benefit. Consider calling the mapper directly.

@rodvar
Copy link
Collaborator

rodvar commented Feb 16, 2026

haha I think we can ignore this 😆

-------------
Total:   1 line
Missing: 1 line
Coverage: 0%
-------------

@rodvar rodvar self-assigned this Feb 16, 2026
@wodoro
Copy link
Contributor Author

wodoro commented Feb 17, 2026

Hey
I'll try to address the bot comments today
will make a PR to upstream with necessary changes afterwards

@rodvar
Copy link
Collaborator

rodvar commented Feb 18, 2026

@wodoro I've did a quick review on this one and it looks good to me. Just concerned about the CRAI Y-plan comment, were you able to have a look into it? what are your thoughts?

@wodoro
Copy link
Contributor Author

wodoro commented Feb 18, 2026

Well most I could do was feed it to different AI and apply the suggestion
I really don't know what's going on, but seems to be working

Copy link
Contributor

@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 (2)
shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/BarcodeAnalyzer.kt (1)

91-117: Double-close of imageProxy in scanInverted failure/cancel paths.

addOnFailureListener and addOnCanceledListener both call imageProxy.close(), and then addOnCompleteListener (which fires unconditionally after any of the above) also calls imageProxy.close(). While ImageProxy.close() is idempotent, this contradicts the intent expressed in the line 115 comment ("CRITICAL: Always close the proxy after the final attempt"). Remove the imageProxy.close() calls from the failure and cancel listeners to let the complete listener be the sole closer—matching the stated design intent.

Proposed fix
             }.addOnFailureListener {
                 onFailed(it)
-                imageProxy.close()
             }.addOnCanceledListener {
                 onCanceled()
-                imageProxy.close()
             }.addOnCompleteListener {
                 // CRITICAL: Always close the proxy after the final attempt
                 imageProxy.close()
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/BarcodeAnalyzer.kt`
around lines 91 - 117, In scanInverted, remove the explicit imageProxy.close()
calls from the addOnFailureListener and addOnCanceledListener so that only the
addOnCompleteListener closes the ImageProxy; keep calling onFailed(it) in the
failure listener and onCanceled() in the canceled listener, and retain the
early-return catch block that already closes imageProxy if
createInvertedInputImage fails—this ensures addOnCompleteListener is the sole
closer as intended.
shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt (1)

184-207: Nit: cameraProviderFuture parameter name is misleading.

This parameter is a ProcessCameraProvider, not a future. A name like cameraProvider or provider would be clearer. Minor since this appears to be inherited code.

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

In `@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`
around lines 184 - 207, The parameter name cameraProviderFuture in the
bindCamera function is misleading (it is a ProcessCameraProvider, not a future);
rename the parameter to cameraProvider (or provider) and update all uses inside
bindCamera (the unbindAll call, the safe-call chain, and bindToLifecycle
invocation) as well as the function signature and any call sites to use the new
name to keep the API clear and consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/BarcodeAnalyzer.kt`:
- Around line 93-99: The catch block around createInvertedInputImage in
BarcodeAnalyzer currently swallows all exceptions; update it to record the
exception before cleaning up so diagnostics are preserved (e.g., call your
logger/Log.e/Timber with a descriptive message and the exception), then close
imageProxy and return; ensure the log includes the imageProxy/state context and
the exception so you can trace null/empty-planes or other failures originating
in createInvertedInputImage.

In
`@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`:
- Around line 141-154: The lambdas passed into BarcodeAnalyzer inside the
AndroidView.factory capture the original result reference and can go stale on
recomposition; replace direct calls to result(...) with the rememberUpdatedState
wrapper (updatedResult) so the latest callback is always invoked (e.g., use
updatedResult.value(...) in the onSuccess, onFailed and onCanceled lambdas
inside the BarcodeAnalyzer instantiation), leaving other behavior (like
provider.unbind(imageAnalysis)) unchanged.

---

Nitpick comments:
In `@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/BarcodeAnalyzer.kt`:
- Around line 91-117: In scanInverted, remove the explicit imageProxy.close()
calls from the addOnFailureListener and addOnCanceledListener so that only the
addOnCompleteListener closes the ImageProxy; keep calling onFailed(it) in the
failure listener and onCanceled() in the canceled listener, and retain the
early-return catch block that already closes imageProxy if
createInvertedInputImage fails—this ensures addOnCompleteListener is the sole
closer as intended.

In
`@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`:
- Around line 184-207: The parameter name cameraProviderFuture in the bindCamera
function is misleading (it is a ProcessCameraProvider, not a future); rename the
parameter to cameraProvider (or provider) and update all uses inside bindCamera
(the unbindAll call, the safe-call chain, and bindToLifecycle invocation) as
well as the function signature and any call sites to use the new name to keep
the API clear and consistent.

Copy link
Contributor

@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)
shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt (1)

156-165: result = result is inconsistent with the updatedResult pattern used elsewhere.

bindCamera only invokes result synchronously within getOrElse during the factory call, so there is no stale-lambda risk here. However, for consistency with the rest of the file, consider forwarding updatedResult:

♻️ Proposed change
-                        result = result,
+                        result = { updatedResult(it) },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`
around lines 156 - 165, The call to bindCamera passes result = result which is
inconsistent with the file's updatedResult pattern; change the argument to
forward updatedResult (i.e., result = updatedResult) so bindCamera receives the
same stabilized lambda used elsewhere (refer to the updatedResult variable and
the bindCamera(...) invocation) to maintain consistency and avoid divergence
between synchronous and updated result handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`:
- Around line 156-165: The call to bindCamera passes result = result which is
inconsistent with the file's updatedResult pattern; change the argument to
forward updatedResult (i.e., result = updatedResult) so bindCamera receives the
same stabilized lambda used elsewhere (refer to the updatedResult variable and
the bindCamera(...) invocation) to maintain consistency and avoid divergence
between synchronous and updated result handlers.

@rodvar
Copy link
Collaborator

rodvar commented Feb 18, 2026

rebased to main

Copy link
Contributor

@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: 1

🧹 Nitpick comments (2)
shared/kscan/src/iosMain/kotlin/org/ncgroup/kscan/CameraViewController.kt (1)

245-268: Ordering note in dispose(): preview layer removal races with background session teardown.

The preview layer is removed from the superlayer synchronously (main thread) while the session stop + input/output removal runs asynchronously on a background queue. This means the session could still be running briefly after its preview layer is gone. In practice this is harmless (the runCatching blocks prevent crashes), but if you observe any visual artifacts during dismissal, consider moving previewLayer.removeFromSuperlayer() into the background block's completion or using a serial queue.

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

In `@shared/kscan/src/iosMain/kotlin/org/ncgroup/kscan/CameraViewController.kt`
around lines 245 - 268, The dispose() method currently removes previewLayer on
the main thread immediately while stopping captureSession asynchronously, which
can race and leave the session running after the layer is gone; move the
previewLayer.removeFromSuperlayer() call so it executes after the background
teardown completes (either by performing it on the same background block after
inputs/outputs are removed, or by dispatching a completion back to main from
that block), ensuring you still guard with ::previewLayer.isInitialized and keep
barcodesDetected.clear() as appropriate; update references to dispose(),
previewLayer, and captureSession accordingly.
shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt (1)

156-165: bindCamera receives the raw result instead of updatedResult.

Inside the factory lambda (which runs once), bindCamera is called with result = result (line 163). Its error handler at line 205 invokes this reference directly. Currently this is safe because bindCamera executes synchronously during factory creation, but it's inconsistent with the updatedResult usage elsewhere in the same block. If bindCamera were ever made asynchronous, this would silently revert to the stale-callback bug.

Suggested fix for consistency
                 camera =
                     bindCamera(
                         lifecycleOwner = lifecycleOwner,
                         cameraProviderFuture = provider,
                         selector = selector,
                         preview = preview,
                         imageAnalysis = imageAnalysis,
-                        result = result,
+                        result = updatedResult,
                         cameraControl = { cameraControl = it },
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`
around lines 156 - 165, The bindCamera call inside the factory currently passes
the original result reference (result) which can become stale; change the call
to pass updatedResult instead (i.e., result = updatedResult) and make sure any
error handler callbacks invoked by bindCamera also reference updatedResult
rather than the original result so the latest callback is used if bindCamera
becomes asynchronous; update uses of result within the factory/error handling to
consistently use updatedResult and preserve the existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shared/kscan/src/iosMain/kotlin/org/ncgroup/kscan/BarcodeFormatMapper.kt`:
- Around line 38-43: The toAvTypes function currently returns an empty list when
none of the requested BarcodeFormat values map via APP_TO_AV_FORMAT_MAP, causing
CameraViewController.setupMetadataOutput to error; to align behavior with
Android's toMlKitFormats, modify toAvTypes to detect this case and fall back to
allSupportedTypes (or explicitly include BarcodeFormat.FORMAT_ALL_FORMATS
semantics) when appFormats is non-empty but mapNotNull yields an empty result,
ensuring callers receive the same lenient default as Android; update references
in toAvTypes, APP_TO_AV_FORMAT_MAP, and tests that exercise
CameraViewController.setupMetadataOutput accordingly.

---

Duplicate comments:
In `@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/BarcodeAnalyzer.kt`:
- Around line 91-99: In scanInverted, don’t silently swallow exceptions from
createInvertedInputImage: either call the existing onFailed(...) fallback with
the caught exception or at minimum log the exception before closing imageProxy
and returning; update the catch block in scanInverted to capture the Exception e
and pass it to onFailed(e) or processLogger/errorLogger with a clear message,
then imageProxy.close() and return so diagnostic context is preserved.
- Around line 118-156: This review is a duplicate approval—no code change is
required in createInvertedInputImage (the Y-plane handling, inversion via
nv21Bytes and neutral chroma fill are correct); remove the duplicate review
comment/mark ([duplicate_comment]) and keep a single [approve_code_changes] so
the PR shows one approval rather than two.

In
`@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`:
- Around line 141-154: The BarcodeAnalyzer callbacks already use updatedResult
to avoid stale references (see BarcodeAnalyzer usage in
imageAnalysis.setAnalyzer and updatedResult calls for
onSuccess/onFailed/onCanceled), so no code change is necessary; mark the earlier
review comment as a duplicate and approve the change.
- Around line 94-98: The change to key LaunchedEffect on initializationError and
to call updatedResult(BarcodeResult.OnFailed(Exception(initializationError))) is
correct; no code change is required—keep the LaunchedEffect block as written
(LaunchedEffect(initializationError) { if (initializationError != null) {
updatedResult(BarcodeResult.OnFailed(Exception(initializationError))) } }) and
remove any duplicate review/comment if present.

---

Nitpick comments:
In
`@shared/kscan/src/androidMain/kotlin/org/ncgroup/kscan/ScannerView.android.kt`:
- Around line 156-165: The bindCamera call inside the factory currently passes
the original result reference (result) which can become stale; change the call
to pass updatedResult instead (i.e., result = updatedResult) and make sure any
error handler callbacks invoked by bindCamera also reference updatedResult
rather than the original result so the latest callback is used if bindCamera
becomes asynchronous; update uses of result within the factory/error handling to
consistently use updatedResult and preserve the existing behavior.

In `@shared/kscan/src/iosMain/kotlin/org/ncgroup/kscan/CameraViewController.kt`:
- Around line 245-268: The dispose() method currently removes previewLayer on
the main thread immediately while stopping captureSession asynchronously, which
can race and leave the session running after the layer is gone; move the
previewLayer.removeFromSuperlayer() call so it executes after the background
teardown completes (either by performing it on the same background block after
inputs/outputs are removed, or by dispatching a completion back to main from
that block), ensuring you still guard with ::previewLayer.isInitialized and keep
barcodesDetected.clear() as appropriate; update references to dispose(),
previewLayer, and captureSession accordingly.

Comment on lines +38 to +43
fun toAvTypes(appFormats: List<BarcodeFormat>): List<AVMetadataObjectType> {
if (appFormats.isEmpty() || appFormats.contains(BarcodeFormat.FORMAT_ALL_FORMATS)) {
return allSupportedTypes
}
return appFormats.mapNotNull { APP_TO_AV_FORMAT_MAP[it] }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Behavioral inconsistency with Android mapper when no formats map successfully.

On Android, toMlKitFormats falls back to FORMAT_ALL_FORMATS when all requested formats are unmappable (e.g., requesting FORMAT_CODABAR which has no iOS equivalent). On iOS, toAvTypes returns an empty list, which then triggers "No supported barcode types selected" error in CameraViewController.setupMetadataOutput.

This may be intentional (fail-fast on iOS vs. lenient on Android), but the inconsistency could surprise callers. Consider either aligning the behavior or documenting the difference.

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

In `@shared/kscan/src/iosMain/kotlin/org/ncgroup/kscan/BarcodeFormatMapper.kt`
around lines 38 - 43, The toAvTypes function currently returns an empty list
when none of the requested BarcodeFormat values map via APP_TO_AV_FORMAT_MAP,
causing CameraViewController.setupMetadataOutput to error; to align behavior
with Android's toMlKitFormats, modify toAvTypes to detect this case and fall
back to allSupportedTypes (or explicitly include
BarcodeFormat.FORMAT_ALL_FORMATS semantics) when appFormats is non-empty but
mapNotNull yields an empty result, ensuring callers receive the same lenient
default as Android; update references in toAvTypes, APP_TO_AV_FORMAT_MAP, and
tests that exercise CameraViewController.setupMetadataOutput accordingly.

Copy link
Collaborator

@rodvar rodvar left a comment

Choose a reason for hiding this comment

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

ACK

this upgrade won't be included in the first connect apps release, but will in the immediate update of any of the apps

we have more time to test it throughfully

@rodvar
Copy link
Collaborator

rodvar commented Feb 18, 2026

ignoring PR coverage check for the reasons above

@rodvar rodvar merged commit 921385e into bisq-network:main Feb 18, 2026
5 of 6 checks passed
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.

2 participants

Comments