Skip to content

Book scan: camera/ISBN capture → reader-affinity → optional insight (Android)#71

Merged
vitofico merged 11 commits into
mainfrom
feat/book-scan-android
May 30, 2026
Merged

Book scan: camera/ISBN capture → reader-affinity → optional insight (Android)#71
vitofico merged 11 commits into
mainfrom
feat/book-scan-android

Conversation

@vitofico

Copy link
Copy Markdown
Owner

Track B of the book-scan feature — the Android client for the deterministic affinity endpoint shipped in #68. Device-validated on a real phone (camera decode confirmed working).

What it does

Catalog tab → scan FABScanScreen: live CameraX preview + ZXing EAN-13 decode, with an always-available manual ISBN field + permission flow. On an ISBN → on-device OpenLibrary metadata lookup → POST /library/v1/affinity → result screen showing score + band + reasons (or "already in your library" / "not enough history" / "affinity unavailable"), plus a "Get full insight" button that reuses the existing InsightSection.

Layers

  • :core:metadata Isbn (ISBN-10/13 canonicalization, mirrors the server) · :data:opds OpenLibraryClient (plain OkHttp, no account auth) · :data:library LibraryClient.affinity() + DTOs (account-authed) · :app ScanViewModel (state machine) + ScanScreen/ScanResultScreen + nav/Catalog FAB/DI.
  • Push-model preserved: ISBN→metadata resolves on-device; quire_server stays metadata-only.

Correctness

Built and hardened with multi-agent adversarial passes (implement → independent reviewers told to refute → fix). Real bugs caught + fixed before they ever shipped: camera rotation (portrait-held horizontal barcode) + YUV row-stride, a rotateCounterClockwise() call that throws in ZXing 3.5.3, single-arg decode() wiping hints, a 401 dead-spinner, a cancellation/stale-result race, and the no-server case now degrades to metadata-only instead of a red error.

Testing

Unit tests for ISBN, OpenLibrary parsing (MockWebServer), affinity client, and the ScanViewModel state machine. Full :app test + lint + assembleDebug green. The camera analyzer itself isn't unit-tested (verified on-device); a Compose UI test could be added now that main ships a Compose test harness.

Known follow-ups (non-blocking)

  • Camera decode is full-frame (no ROI crop / 180° variant) — works in practice; tune if field misses appear.
  • Debug/release side-by-side install config lands in a separate small PR.

Requires the affinity endpoint (#68, merged) deployed to the server the client targets.

vitofico added 11 commits May 30, 2026 19:06
- Run lookup() on Dispatchers.IO; the OpenLibrary client does a blocking
  OkHttp call without hopping dispatchers, so the VM must. Correct the KDoc
  that wrongly claimed it hopped internally.
- Replace the 401 dead-spinner (re-emitting Working) with a recoverable
  ScanUiState.ReauthRequired state after firing onReauth().
- Guard every post-suspension state write with a per-scan generation check
  and rethrow CancellationException, so a slow/superseded scan can never
  clobber a newer scan's result (matches LibraryStatsViewModel's pattern).

Add regression tests: 401 -> ReauthRequired (not stuck Working), stale scan
does not clobber a newer result, and lookup runs off the main thread.
@vitofico
vitofico merged commit 1284f5a into main May 30, 2026
5 of 11 checks passed
@vitofico
vitofico deleted the feat/book-scan-android branch May 30, 2026 21:10
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