From 93f972b7d18a78a10d8b363be7dfa251b19bb884 Mon Sep 17 00:00:00 2001 From: Sprite Date: Wed, 9 Sep 2026 12:44:29 +0000 Subject: [PATCH] Distinguish can't-scan, can't-look-up, and genuinely-not-found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented by a Sonnet worker (tasks/G-diagnostics.txt) against the three-way contract added to SPEC in 1969b74; independently re-verified by the orchestrator rather than accepted on the worker's own report. The app previously conflated three outcomes. A barcode that failed the ISBN-13 checksum produced NOTHING — no sheet, no message — which is indistinguishable from a dead camera. A lookup that failed at the transport layer (Google Books answers HTTP 429 to keyless callers) was reported as "No match found", telling the user a book does not exist when the app never managed to ask. Only the third case was ever honest. Now: each client returns SourceResult{Found,NotFound,Failed} from a pure classify() so the status-code matrix is testable offline without a MockWebServer; MetadataRepository.combine folds those into LookupResult{Found,NotFound,Unavailable}, where NotFound requires EVERY source to have answered authoritatively. A rejected barcode raises a throttled banner on the camera screen, sharing ScanCodeFilter's existing debounce so a non-book barcode sitting in frame shows the message once instead of flickering per analyzed frame. A failed lookup gets its own sheet with Retry / Enter by hand / Skip that never claims the book is unknown. The metadata OkHttpClient finally has a call timeout. Orchestrator's own addition: the manual-ISBN dialog silently discarded an unparseable entry — the same silent failure on the same screen, missed because it sat just outside the worker's brief. It now marks the field in error and disables Look up until the checksum passes. assembleDebug + verifyPaparazziDebug exit 0; 138 tests, 1 skipped, 0 failures (was 107). Boundary check clean: no build files, no data/local, data/remote, data/repo, ui/settings, ui/locations or ui/detail. Co-Authored-By: Claude Opus 5 --- .../java/org/modg/bookshelf/AppContainer.kt | 19 ++- .../data/metadata/GoogleBooksClient.kt | 47 ++++--- .../bookshelf/data/metadata/LookupResult.kt | 18 +++ .../data/metadata/MetadataRepository.kt | 50 ++++++-- .../data/metadata/OpenLibraryClient.kt | 48 +++++--- .../bookshelf/data/metadata/SourceResult.kt | 23 ++++ .../modg/bookshelf/ui/scan/ScanCodeFilter.kt | 44 +++++-- .../org/modg/bookshelf/ui/scan/ScanModels.kt | 25 +++- .../org/modg/bookshelf/ui/scan/ScanScreen.kt | 116 ++++++++++++++++-- .../modg/bookshelf/ui/scan/ScanViewModel.kt | 43 ++++++- .../bookshelf/ui/scan/ScannerController.kt | 14 ++- .../data/metadata/GoogleBooksClientTest.kt | 41 +++++++ .../data/metadata/MetadataRepositoryTest.kt | 74 +++++++++++ .../data/metadata/OpenLibraryClientTest.kt | 41 +++++++ .../bookshelf/ui/scan/ScanCodeFilterTest.kt | 85 ++++++++++--- .../modg/bookshelf/ui/scan/ScanModelsTest.kt | 15 ++- .../ui/scan/ScannerControllerTest.kt | 33 +++++ .../ui/screens/ScanScreenPaparazziTest.kt | 39 ++++++ ...eetLight_scan-lookup-failed-sheet-dark.png | Bin 0 -> 28681 bytes ...etLight_scan-lookup-failed-sheet-light.png | Bin 0 -> 28793 bytes ...arcodeLight_scan-rejected-barcode-dark.png | Bin 0 -> 19420 bytes ...rcodeLight_scan-rejected-barcode-light.png | Bin 0 -> 19253 bytes 22 files changed, 676 insertions(+), 99 deletions(-) create mode 100644 app/app/src/main/java/org/modg/bookshelf/data/metadata/LookupResult.kt create mode 100644 app/app/src/main/java/org/modg/bookshelf/data/metadata/SourceResult.kt create mode 100644 app/app/src/test/snapshots/images/org.modg.bookshelf.ui.screens_ScanScreenPaparazziTest_scanLookupFailedSheetLight_scan-lookup-failed-sheet-dark.png create mode 100644 app/app/src/test/snapshots/images/org.modg.bookshelf.ui.screens_ScanScreenPaparazziTest_scanLookupFailedSheetLight_scan-lookup-failed-sheet-light.png create mode 100644 app/app/src/test/snapshots/images/org.modg.bookshelf.ui.screens_ScanScreenPaparazziTest_scanRejectedBarcodeLight_scan-rejected-barcode-dark.png create mode 100644 app/app/src/test/snapshots/images/org.modg.bookshelf.ui.screens_ScanScreenPaparazziTest_scanRejectedBarcodeLight_scan-rejected-barcode-light.png diff --git a/app/app/src/main/java/org/modg/bookshelf/AppContainer.kt b/app/app/src/main/java/org/modg/bookshelf/AppContainer.kt index 2df9d24..b8ea20a 100644 --- a/app/app/src/main/java/org/modg/bookshelf/AppContainer.kt +++ b/app/app/src/main/java/org/modg/bookshelf/AppContainer.kt @@ -1,6 +1,7 @@ package org.modg.bookshelf import android.content.Context +import java.util.concurrent.TimeUnit import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex @@ -86,11 +87,19 @@ class AppContainer(private val context: Context) { val authRepository by lazy { AuthRepository(apiProvider, settingsStore) } - // A bare client — deliberately NOT [okHttpClient] above, which carries our - // PocketBase bearer token via PbAuthInterceptor. Open Library/Google Books - // are third-party services; that token must never leave this device's - // requests to our own server. - private val metadataHttpClient: OkHttpClient by lazy { OkHttpClient() } + // Deliberately NOT [okHttpClient] above, which carries our PocketBase bearer + // token via PbAuthInterceptor. Open Library/Google Books are third-party + // services; that token must never leave this device's requests to our own + // server. A call timeout is load-bearing here: with none, a stalled + // connection hangs on OkHttp's default (unbounded) socket timeouts, and the + // user is standing at a bookshelf waiting on it. + private val metadataHttpClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .callTimeout(12, TimeUnit.SECONDS) + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build() + } val metadataRepository by lazy { MetadataRepository(metadataHttpClient, json) } diff --git a/app/app/src/main/java/org/modg/bookshelf/data/metadata/GoogleBooksClient.kt b/app/app/src/main/java/org/modg/bookshelf/data/metadata/GoogleBooksClient.kt index 604cfa5..ff61fc3 100644 --- a/app/app/src/main/java/org/modg/bookshelf/data/metadata/GoogleBooksClient.kt +++ b/app/app/src/main/java/org/modg/bookshelf/data/metadata/GoogleBooksClient.kt @@ -1,6 +1,7 @@ package org.modg.bookshelf.data.metadata import java.io.IOException +import java.net.SocketTimeoutException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.SerializationException @@ -10,7 +11,8 @@ import okhttp3.Request /** * Google Books lookup — SPEC.md "Book metadata lookup" fallback source. No API key. - * Never throws: network/parse failures fail soft and return null. + * Never throws: every outcome, including transport failure, comes back as a + * [SourceResult] rather than a swallowed null. */ class GoogleBooksClient( private val httpClient: OkHttpClient, @@ -18,29 +20,42 @@ class GoogleBooksClient( ) { private val json = Json(from = json) { ignoreUnknownKeys = true } - suspend fun lookup(isbn13: String): BookMetadata? = withContext(Dispatchers.IO) { - val body = fetchBody(isbn13) ?: return@withContext null - parseResponse(body) - } + suspend fun lookup(isbn13: String): SourceResult = withContext(Dispatchers.IO) { fetch(isbn13) } - private fun fetchBody(isbn13: String): String? = try { + private fun fetch(isbn13: String): SourceResult = try { val request = Request.Builder() .url("https://www.googleapis.com/books/v1/volumes?q=isbn:$isbn13") .build() httpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) null else response.body?.string() + classify(response.code, response.body.string()) } + } catch (e: SocketTimeoutException) { + SourceResult.Failed("timeout") } catch (e: IOException) { - null + SourceResult.Failed("network error") + } + + /** + * Package-visible pure function — no socket involved — so it's exhaustively + * unit-testable offline (2xx-with-record, 2xx-without-record, 404, 429, 500, + * malformed body). [parseResponse] is defined in terms of this so the two + * can never disagree about what a body means. + */ + internal fun classify(httpCode: Int, body: String?): SourceResult { + if (httpCode !in 200..299) return SourceResult.Failed("http $httpCode") + if (body.isNullOrBlank()) return SourceResult.Failed("empty body") + return try { + val dto = json.decodeFromString(GoogleBooksResponseDto.serializer(), body) + val metadata = dto.items.firstOrNull()?.volumeInfo?.toBookMetadata() + if (metadata != null) SourceResult.Found(metadata) else SourceResult.NotFound + } catch (e: SerializationException) { + SourceResult.Failed("malformed json") + } catch (e: IllegalArgumentException) { + SourceResult.Failed("malformed json") + } } /** Package-visible for offline fixture tests — parses a raw response body with no network involved. */ - internal fun parseResponse(body: String): BookMetadata? = try { - val dto = json.decodeFromString(GoogleBooksResponseDto.serializer(), body) - dto.items.firstOrNull()?.volumeInfo?.toBookMetadata() - } catch (e: SerializationException) { - null - } catch (e: IllegalArgumentException) { - null - } + internal fun parseResponse(body: String): BookMetadata? = + (classify(200, body) as? SourceResult.Found)?.metadata } diff --git a/app/app/src/main/java/org/modg/bookshelf/data/metadata/LookupResult.kt b/app/app/src/main/java/org/modg/bookshelf/data/metadata/LookupResult.kt new file mode 100644 index 0000000..acbb6ad --- /dev/null +++ b/app/app/src/main/java/org/modg/bookshelf/data/metadata/LookupResult.kt @@ -0,0 +1,18 @@ +package org.modg.bookshelf.data.metadata + +/** + * The combined outcome of a metadata lookup across both sources (SPEC.md "Book + * metadata lookup"). Produced by [MetadataRepository.lookup] from the two + * [SourceResult]s per [MetadataRepository.combine]'s rules: + * - any source [SourceResult.Found] -> [Found] + * - every source [SourceResult.NotFound] -> [NotFound] + * - otherwise (at least one [SourceResult.Failed], none Found) -> [Unavailable] + * + * The last rule is the whole point: one reachable source answering "no" is not + * authoritative while the other source couldn't be asked at all. + */ +sealed interface LookupResult { + data class Found(val metadata: BookMetadata) : LookupResult + data object NotFound : LookupResult + data class Unavailable(val reason: String) : LookupResult +} diff --git a/app/app/src/main/java/org/modg/bookshelf/data/metadata/MetadataRepository.kt b/app/app/src/main/java/org/modg/bookshelf/data/metadata/MetadataRepository.kt index 800af60..82680cf 100644 --- a/app/app/src/main/java/org/modg/bookshelf/data/metadata/MetadataRepository.kt +++ b/app/app/src/main/java/org/modg/bookshelf/data/metadata/MetadataRepository.kt @@ -7,9 +7,10 @@ import okhttp3.OkHttpClient /** * Single entry point for book metadata lookup (SPEC.md "Book metadata lookup"). - * Queries both sources concurrently and merges per [MetadataMerger]. Returns null - * if [isbn] doesn't checksum-validate or if both sources miss — callers (the scan - * screen) must then fall back to manual entry pre-filled with the scanned ISBN. + * Queries both sources concurrently and combines their [SourceResult]s into one + * [LookupResult] per [combine]. [isbn] must already be a checksum-valid ISBN-10/13 + * by the time it reaches here — the scan layer is responsible for that, and an + * invalid one is a programming error at this layer, not a lookup outcome. */ class MetadataRepository( private val openLibraryClient: OpenLibraryClient, @@ -20,21 +21,46 @@ class MetadataRepository( GoogleBooksClient(httpClient, json), ) - suspend fun lookup(isbn: String): BookMetadata? { - val isbn13 = IsbnUtils.toIsbn13(isbn) ?: return null - val merged = coroutineScope { + suspend fun lookup(isbn: String): LookupResult { + val isbn13 = checkNotNull(IsbnUtils.toIsbn13(isbn)) { + "MetadataRepository.lookup requires an already-validated ISBN-10/13; got: $isbn" + } + return coroutineScope { val openLibrary = async { openLibraryClient.lookup(isbn13) } val googleBooks = async { googleBooksClient.lookup(isbn13) } - MetadataMerger.merge(openLibrary.await(), googleBooks.await()) - } ?: return null - return if (merged.coverUrl.isNullOrBlank()) { - merged.copy(coverUrl = byIsbnCoverUrl(isbn13)) - } else { - merged + combine(openLibrary.await(), googleBooks.await(), isbn13) } } companion object { + /** + * Combines the two sources' outcomes per SPEC.md's three-way rule: any Found + * wins (merged via [MetadataMerger] and given the last-resort cover), all + * NotFound is an honest miss, and anything else — at least one Failed and + * nothing Found — is Unavailable, because a source that couldn't be reached + * must never be reported as a book that doesn't exist. Package-visible and + * pure (no I/O) so the full 3x3 matrix is unit-testable offline. + */ + internal fun combine(openLibrary: SourceResult, googleBooks: SourceResult, isbn13: String): LookupResult { + val merged = MetadataMerger.merge( + (openLibrary as? SourceResult.Found)?.metadata, + (googleBooks as? SourceResult.Found)?.metadata, + ) + if (merged != null) { + val withCover = if (merged.coverUrl.isNullOrBlank()) { + merged.copy(coverUrl = byIsbnCoverUrl(isbn13)) + } else { + merged + } + return LookupResult.Found(withCover) + } + val failures = listOfNotNull( + (openLibrary as? SourceResult.Failed)?.reason?.let { "open library: $it" }, + (googleBooks as? SourceResult.Failed)?.reason?.let { "google books: $it" }, + ) + return if (failures.isNotEmpty()) LookupResult.Unavailable(failures.joinToString("; ")) else LookupResult.NotFound + } + /** * Last resort when neither source reported cover art, per SPEC's cover chain. * `default=false` is load-bearing: without it this endpoint answers 200 with a diff --git a/app/app/src/main/java/org/modg/bookshelf/data/metadata/OpenLibraryClient.kt b/app/app/src/main/java/org/modg/bookshelf/data/metadata/OpenLibraryClient.kt index 5383487..fefe696 100644 --- a/app/app/src/main/java/org/modg/bookshelf/data/metadata/OpenLibraryClient.kt +++ b/app/app/src/main/java/org/modg/bookshelf/data/metadata/OpenLibraryClient.kt @@ -1,6 +1,7 @@ package org.modg.bookshelf.data.metadata import java.io.IOException +import java.net.SocketTimeoutException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.SerializationException @@ -12,7 +13,8 @@ import okhttp3.Request /** * Open Library lookup — SPEC.md "Book metadata lookup" primary source. - * Never throws: network/parse failures fail soft and return null. + * Never throws: every outcome, including transport failure, comes back as a + * [SourceResult] rather than a swallowed null. */ class OpenLibraryClient( private val httpClient: OkHttpClient, @@ -21,30 +23,42 @@ class OpenLibraryClient( // Real responses carry fields this DTO doesn't model; never let an unknown key throw. private val json = Json(from = json) { ignoreUnknownKeys = true } - suspend fun lookup(isbn13: String): BookMetadata? = withContext(Dispatchers.IO) { - val body = fetchBody(isbn13) ?: return@withContext null - parseResponse(body, isbn13) - } + suspend fun lookup(isbn13: String): SourceResult = withContext(Dispatchers.IO) { fetch(isbn13) } - private fun fetchBody(isbn13: String): String? = try { + private fun fetch(isbn13: String): SourceResult = try { val request = Request.Builder() .url("https://openlibrary.org/api/books?bibkeys=ISBN:$isbn13&format=json&jscmd=data") .build() httpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) null else response.body?.string() + classify(response.code, response.body.string(), isbn13) } + } catch (e: SocketTimeoutException) { + SourceResult.Failed("timeout") } catch (e: IOException) { - null + SourceResult.Failed("network error") + } + + /** + * Package-visible pure function — no socket involved — so it's exhaustively + * unit-testable offline (2xx-with-record, 2xx-without-record, 404, 429, 500, + * malformed body). [parseResponse] is defined in terms of this so the two + * can never disagree about what a body means. + */ + internal fun classify(httpCode: Int, body: String?, isbn13: String): SourceResult { + if (httpCode !in 200..299) return SourceResult.Failed("http $httpCode") + if (body.isNullOrBlank()) return SourceResult.Failed("empty body") + return try { + val root = json.parseToJsonElement(body).jsonObject + val entry = root["ISBN:$isbn13"]?.jsonObject ?: return SourceResult.NotFound + SourceResult.Found(json.decodeFromJsonElement(entry).toBookMetadata(isbn13)) + } catch (e: SerializationException) { + SourceResult.Failed("malformed json") + } catch (e: IllegalArgumentException) { + SourceResult.Failed("malformed json") + } } /** Package-visible for offline fixture tests — parses a raw response body with no network involved. */ - internal fun parseResponse(body: String, isbn13: String): BookMetadata? = try { - val root = json.parseToJsonElement(body).jsonObject - val entry = root["ISBN:$isbn13"]?.jsonObject ?: return null - json.decodeFromJsonElement(entry).toBookMetadata(isbn13) - } catch (e: SerializationException) { - null - } catch (e: IllegalArgumentException) { - null - } + internal fun parseResponse(body: String, isbn13: String): BookMetadata? = + (classify(200, body, isbn13) as? SourceResult.Found)?.metadata } diff --git a/app/app/src/main/java/org/modg/bookshelf/data/metadata/SourceResult.kt b/app/app/src/main/java/org/modg/bookshelf/data/metadata/SourceResult.kt new file mode 100644 index 0000000..bd4ed25 --- /dev/null +++ b/app/app/src/main/java/org/modg/bookshelf/data/metadata/SourceResult.kt @@ -0,0 +1,23 @@ +package org.modg.bookshelf.data.metadata + +/** + * Per-source lookup outcome (SPEC.md "Book metadata lookup": "Lookup outcome is + * THREE-WAY, never a bare null"). [OpenLibraryClient] and [GoogleBooksClient] each + * produce one of these instead of collapsing "couldn't be reached" and "answered, + * doesn't have it" into the same `null`. [MetadataRepository] combines the two + * source results into a [LookupResult]. + */ +sealed interface SourceResult { + data class Found(val metadata: BookMetadata) : SourceResult + + /** The source answered (2xx) and, in good faith, has no record for this ISBN. */ + data object NotFound : SourceResult + + /** + * The source could not be asked, or its answer couldn't be trusted: non-2xx, + * timeout, transport error, or a body that didn't parse. [reason] is a short + * diagnostic ("http 429", "timeout", "malformed json") for logs — never shown + * to the user verbatim. + */ + data class Failed(val reason: String) : SourceResult +} diff --git a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanCodeFilter.kt b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanCodeFilter.kt index 58ebb8e..9f00253 100644 --- a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanCodeFilter.kt +++ b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanCodeFilter.kt @@ -2,11 +2,31 @@ package org.modg.bookshelf.ui.scan import org.modg.bookshelf.data.metadata.IsbnUtils +/** What a single decoded barcode read means, once debouncing has been applied. */ +sealed interface ScanOutcome { + /** A checksum-valid ISBN-13, ready for metadata lookup. */ + data class Isbn(val isbn13: String) : ScanOutcome + + /** + * Decoded fine, but its checksum rules it out as a book ISBN (SPEC.md + * "Barcode scanning": "ignore non-book barcodes" — but not silently, see + * [rawValue]). Carries the raw value so the camera screen can echo back what + * it read. + */ + data class NotAnIsbn(val rawValue: String) : ScanOutcome + + /** A debounced repeat of the last code (valid or not) — emit no UI at all. */ + data object Ignored : ScanOutcome +} + /** - * Pure decode-to-ISBN pipeline shared by the analyzer and its tests: normalizes a raw - * barcode value, validates the ISBN-13 checksum (this is what makes EAN_8/UPC_A reads - * fall out as "non-book barcodes" per SPEC.md "Barcode scanning" — they can never be - * 13 digits), and debounces repeat reads of the same code. + * Pure decode-to-outcome pipeline shared by the analyzer and its tests: normalizes + * a raw barcode value, classifies it as a valid ISBN-13 or not (this is what makes + * EAN_8/UPC_A reads fall out as "non-book barcodes" per SPEC.md "Barcode scanning" + * — they can never be 13 digits), and debounces repeats of the same code, whether + * or not it validated. The debounce is what keeps a non-book barcode sitting in + * frame — which decodes on nearly every analyzed frame — from flickering a + * rejection message instead of showing it once. */ class ScanCodeFilter( private val debounceMillis: Long = 2000L, @@ -15,15 +35,19 @@ class ScanCodeFilter( private var lastCode: String? = null private var lastEmitMillis: Long = Long.MIN_VALUE - /** Returns the normalized ISBN-13 if [rawValue] is a valid, non-debounced hit; null otherwise. */ - fun accept(rawValue: String?): String? { - val normalized = IsbnUtils.normalize(rawValue ?: return null) - if (!IsbnUtils.isValidIsbn13(normalized)) return null + /** Classifies [rawValue] per [ScanOutcome], applying the debounce window. */ + fun accept(rawValue: String?): ScanOutcome { + val raw = rawValue ?: return ScanOutcome.Ignored + val normalized = IsbnUtils.normalize(raw) val now = nowMillis() - if (normalized == lastCode && now - lastEmitMillis < debounceMillis) return null + if (normalized == lastCode && now - lastEmitMillis < debounceMillis) return ScanOutcome.Ignored lastCode = normalized lastEmitMillis = now - return normalized + return if (IsbnUtils.isValidIsbn13(normalized)) { + ScanOutcome.Isbn(normalized) + } else { + ScanOutcome.NotAnIsbn(raw) + } } /** Allows the next scan of any code (including a repeat) to emit immediately, e.g. after Skip. */ diff --git a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanModels.kt b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanModels.kt index 8d9325e..f6ce03d 100644 --- a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanModels.kt +++ b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanModels.kt @@ -2,6 +2,7 @@ package org.modg.bookshelf.ui.scan import org.modg.bookshelf.data.local.BookEntity import org.modg.bookshelf.data.metadata.BookMetadata +import org.modg.bookshelf.data.metadata.LookupResult /** SPEC.md scan screen: "Duplicate-ISBN warning if already owned." */ sealed interface DuplicateStatus { @@ -27,14 +28,30 @@ sealed interface ScanSheetState { */ data class Loading(val isbn13: String) : ScanSheetState data class Found(val isbn13: String, val metadata: BookMetadata, val duplicate: DuplicateStatus) : ScanSheetState - data class NotFound(val isbn13: String) : ScanSheetState + + /** + * Every source answered and none had the book (SPEC: an honest "no", not a + * cover story for a failed request). [viaLookupFailure] is true only when the + * user reached this form through [LookupFailed]'s "Enter by hand" escape hatch + * rather than a genuine miss, so the sheet's copy can stop short of claiming + * the book is unknown. + */ + data class NotFound(val isbn13: String, val viaLookupFailure: Boolean = false) : ScanSheetState + + /** + * At least one source couldn't be reached (non-2xx, timeout, transport error) + * and neither had the book — SPEC: "must NOT claim the book is unknown". Offers + * retry, manual entry, and skip instead of a manual-entry form captioned as a miss. + */ + data class LookupFailed(val isbn13: String, val reason: String) : ScanSheetState } /** Combines a metadata lookup result with duplicate status into the sheet state to show. */ object ScanMetadataOutcome { - fun from(isbn13: String, metadata: BookMetadata?, duplicate: DuplicateStatus): ScanSheetState = when (metadata) { - null -> ScanSheetState.NotFound(isbn13) - else -> ScanSheetState.Found(isbn13, metadata, duplicate) + fun from(isbn13: String, result: LookupResult, duplicate: DuplicateStatus): ScanSheetState = when (result) { + is LookupResult.Found -> ScanSheetState.Found(isbn13, result.metadata, duplicate) + is LookupResult.NotFound -> ScanSheetState.NotFound(isbn13) + is LookupResult.Unavailable -> ScanSheetState.LookupFailed(isbn13, result.reason) } } diff --git a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanScreen.kt b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanScreen.kt index 839c7e2..bcb8dd3 100644 --- a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanScreen.kt +++ b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.outlined.FlashOff @@ -44,6 +45,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -58,6 +60,7 @@ import org.modg.bookshelf.AppContainer import org.modg.bookshelf.data.local.BookcaseEntity import org.modg.bookshelf.data.local.ShelfEntity import org.modg.bookshelf.data.metadata.BookMetadata +import org.modg.bookshelf.data.metadata.IsbnUtils import org.modg.bookshelf.ui.components.BookCover import org.modg.bookshelf.ui.components.BookshelfScaffold import org.modg.bookshelf.ui.components.EmptyState @@ -89,6 +92,7 @@ fun ScanScreen( val sheetState by viewModel.sheetState.collectAsState() val sessionState by viewModel.sessionState.collectAsState() + val rejectedMessage by viewModel.rejectedMessage.collectAsState() val torchEnabled by viewModel.scannerController.torchEnabled.collectAsState() val bookcases by viewModel.bookcases.collectAsState() val shelves by viewModel.shelves.collectAsState() @@ -128,6 +132,12 @@ fun ScanScreen( CameraPreview(controller = viewModel.scannerController, torchEnabled = torchEnabled) ScanReticle(modifier = Modifier.align(Alignment.Center)) SessionBadge(count = sessionState.savedCount, modifier = Modifier.align(Alignment.TopCenter).padding(16.dp)) + rejectedMessage?.let { message -> + RejectedBarcodeBanner( + message = message, + modifier = Modifier.align(Alignment.BottomCenter).padding(24.dp), + ) + } } else { PermissionDeniedContent(onRequestAgain = { permissionState.launchPermissionRequest() }) } @@ -160,6 +170,7 @@ fun ScanScreen( ) { ManualEntrySheet( isbn13 = state.isbn13, + authoritative = !state.viaLookupFailure, bookcases = bookcases, shelves = shelves, selectedShelfId = selectedShelfId, @@ -168,6 +179,17 @@ fun ScanScreen( onSkip = { viewModel.skip() }, ) } + is ScanSheetState.LookupFailed -> ModalBottomSheet( + onDismissRequest = { viewModel.dismissSheet() }, + sheetState = rememberModalBottomSheetState(), + ) { + LookupFailedSheet( + isbn13 = state.isbn13, + onRetry = { viewModel.retryLookup(state.isbn13) }, + onEnterByHand = { viewModel.enterByHand(state.isbn13) }, + onSkip = { viewModel.skip() }, + ) + } } if (showManualEntry) { @@ -340,9 +362,16 @@ internal fun FoundBookSheet( } } +/** + * Shown when every source answered and none had the book — SPEC: word this as the + * authoritative negative it is, not as a generic failure. [authoritative] is false + * only when this form was reached via [LookupFailedSheet]'s "Enter by hand", where + * the lookup never completed and the copy must not imply the book is unknown. + */ @Composable internal fun ManualEntrySheet( isbn13: String, + authoritative: Boolean, bookcases: List, shelves: List, selectedShelfId: String?, @@ -354,9 +383,12 @@ internal fun ManualEntrySheet( var authors by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { - Text(text = "No match found", style = MaterialTheme.typography.titleLarge) Text( - text = "ISBN $isbn13 — enter the details by hand.", + text = if (authoritative) "Not in Open Library or Google Books" else "Enter the details by hand", + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = if (authoritative) "ISBN $isbn13 — enter the details by hand." else "ISBN $isbn13", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 12.dp), @@ -393,6 +425,54 @@ internal fun ManualEntrySheet( } } +/** + * Shown when a lookup couldn't be completed — SPEC: "a retry affordance, with + * manual entry as the escape hatch; it must NOT claim the book is unknown." No + * shelf picker here: saving isn't offered from this sheet, only a path onward to + * one that does (Retry, or the manual-entry form via "Enter by hand"). + */ +@Composable +internal fun LookupFailedSheet( + isbn13: String, + onRetry: () -> Unit, + onEnterByHand: () -> Unit, + onSkip: () -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Text(text = "Couldn't complete the lookup", style = MaterialTheme.typography.titleLarge) + Text( + text = "ISBN $isbn13 — one or more sources couldn't be reached. " + + "This doesn't mean the book is unknown.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp), + ) + PrimaryButton(text = "Retry", onClick = onRetry, modifier = Modifier.fillMaxWidth()) + Row(modifier = Modifier.fillMaxWidth().padding(top = 12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + SecondaryButton(text = "Enter by hand", onClick = onEnterByHand, modifier = Modifier.weight(1f)) + SecondaryButton(text = "Skip", onClick = onSkip, modifier = Modifier.weight(1f)) + } + } +} + +/** + * Transient banner for a decoded-but-rejected barcode (SPEC.md "Barcode scanning": + * "A rejected barcode is NOT silent"). [ScanViewModel] owns the debounce (via + * [ScanCodeFilter]) and the auto-clear timer behind [message] — this composable + * just renders whatever it's handed. + */ +@Composable +internal fun RejectedBarcodeBanner(message: String, modifier: Modifier = Modifier) { + Text( + text = message, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = modifier + .background(MaterialTheme.colorScheme.errorContainer, RoundedCornerShape(20.dp)) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) +} + @Composable internal fun ShelfPicker( bookcases: List, @@ -431,19 +511,37 @@ internal fun ShelfPicker( @Composable private fun ManualIsbnDialog(onDismiss: () -> Unit, onSubmit: (String) -> Unit) { var text by remember { mutableStateOf("") } + // Validate here rather than letting ScanViewModel.manualIsbnEntered drop an + // unparseable ISBN on the floor. A typed-in check digit is easy to get wrong, + // and a dialog whose button does nothing is the same silent failure this whole + // screen was just rebuilt to eliminate. + val isbn13 = remember(text) { IsbnUtils.toIsbn13(text) } + val malformed = text.isNotBlank() && isbn13 == null AlertDialog( onDismissRequest = onDismiss, title = { Text("Enter ISBN") }, text = { - OutlinedTextField( - value = text, - onValueChange = { text = it }, - label = { Text("ISBN-10 or ISBN-13") }, - singleLine = true, - ) + Column { + OutlinedTextField( + value = text, + onValueChange = { text = it }, + label = { Text("ISBN-10 or ISBN-13") }, + singleLine = true, + isError = malformed, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + if (malformed) { + Text( + text = "That isn't a valid ISBN — check the digits.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(top = 4.dp), + ) + } + } }, confirmButton = { - PrimaryButton(text = "Look up", onClick = { onSubmit(text) }, enabled = text.isNotBlank()) + PrimaryButton(text = "Look up", onClick = { onSubmit(text) }, enabled = isbn13 != null) }, dismissButton = { SecondaryButton(text = "Cancel", onClick = onDismiss) diff --git a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanViewModel.kt b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanViewModel.kt index b7b834e..7bd7194 100644 --- a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanViewModel.kt +++ b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScanViewModel.kt @@ -2,6 +2,8 @@ package org.modg.bookshelf.ui.scan import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -35,6 +37,11 @@ class ScanViewModel( private val _sessionState = MutableStateFlow(ScanSessionState()) val sessionState: StateFlow = _sessionState.asStateFlow() + private val _rejectedMessage = MutableStateFlow(null) + /** Transient "read but not a book barcode" message for the camera overlay; auto-clears. */ + val rejectedMessage: StateFlow = _rejectedMessage.asStateFlow() + private var rejectedMessageClearJob: Job? = null + val bookcases: StateFlow> = locationRepository.observeBookcases() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) val shelves: StateFlow> = locationRepository.observeShelves() @@ -48,14 +55,41 @@ class ScanViewModel( viewModelScope.launch { scannerController.scanResults.collect { isbn13 -> onScanned(isbn13) } } + viewModelScope.launch { + scannerController.rejectedCodes.collect { rawValue -> showRejectedMessage(rawValue) } + } } private suspend fun onScanned(isbn13: String) { if (_sheetState.value !is ScanSheetState.Hidden) return // a sheet is already up for a previous hit + runLookup(isbn13) + } + + private suspend fun runLookup(isbn13: String) { _sheetState.value = ScanSheetState.Loading(isbn13) val duplicate = DuplicateCheck.check(bookRepository.findByIsbn13(isbn13)) - val metadata = metadataRepository.lookup(isbn13) - _sheetState.value = ScanMetadataOutcome.from(isbn13, metadata, duplicate) + val result = metadataRepository.lookup(isbn13) + _sheetState.value = ScanMetadataOutcome.from(isbn13, result, duplicate) + } + + /** [ScanSheetState.LookupFailed]'s Retry — re-enters loading and re-runs the same lookup. */ + fun retryLookup(isbn13: String) { + viewModelScope.launch { runLookup(isbn13) } + } + + /** [ScanSheetState.LookupFailed]'s "Enter by hand" — falls through to the manual-entry form. */ + fun enterByHand(isbn13: String) { + _sheetState.value = ScanSheetState.NotFound(isbn13, viaLookupFailure = true) + } + + /** Throttled per [ScanCodeFilter]'s debounce; this just owns the auto-clear timer on top. */ + private fun showRejectedMessage(rawValue: String) { + rejectedMessageClearJob?.cancel() + _rejectedMessage.value = "Read $rawValue — not a book barcode" + rejectedMessageClearJob = viewModelScope.launch { + delay(REJECTED_MESSAGE_MILLIS) + _rejectedMessage.value = null + } } /** The manual-ISBN-entry escape hatch (SPEC: for when a barcode won't scan). */ @@ -114,4 +148,9 @@ class ScanViewModel( _sheetState.value = ScanSheetState.Hidden scannerController.resetDebounce() } + + private companion object { + /** How long a rejected-barcode message stays on screen before it auto-clears. */ + const val REJECTED_MESSAGE_MILLIS = 3000L + } } diff --git a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScannerController.kt b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScannerController.kt index 703bde6..911c400 100644 --- a/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScannerController.kt +++ b/app/app/src/main/java/org/modg/bookshelf/ui/scan/ScannerController.kt @@ -19,6 +19,14 @@ class ScannerController( /** Emits a debounced, checksum-valid ISBN-13 each time [IsbnBarcodeAnalyzer] sees a new book barcode. */ val scanResults: SharedFlow = _scanResults.asSharedFlow() + private val _rejectedCodes = MutableSharedFlow(extraBufferCapacity = 1) + /** + * Emits the raw value of a debounced, non-book barcode read (SPEC.md "Barcode + * scanning": "A rejected barcode is NOT silent"). [ScanOutcome.Ignored] reads + * (debounced repeats) never reach here. + */ + val rejectedCodes: SharedFlow = _rejectedCodes.asSharedFlow() + private val _torchEnabled = MutableStateFlow(false) val torchEnabled: StateFlow = _torchEnabled.asStateFlow() @@ -27,7 +35,11 @@ class ScannerController( /** Called by [IsbnBarcodeAnalyzer] for every decoded barcode's raw value. */ fun onBarcodeScanned(rawValue: String?) { - codeFilter.accept(rawValue)?.let { _scanResults.tryEmit(it) } + when (val outcome = codeFilter.accept(rawValue)) { + is ScanOutcome.Isbn -> _scanResults.tryEmit(outcome.isbn13) + is ScanOutcome.NotAnIsbn -> _rejectedCodes.tryEmit(outcome.rawValue) + ScanOutcome.Ignored -> Unit + } } fun toggleTorch() { diff --git a/app/app/src/test/java/org/modg/bookshelf/data/metadata/GoogleBooksClientTest.kt b/app/app/src/test/java/org/modg/bookshelf/data/metadata/GoogleBooksClientTest.kt index 237d75c..26611a3 100644 --- a/app/app/src/test/java/org/modg/bookshelf/data/metadata/GoogleBooksClientTest.kt +++ b/app/app/src/test/java/org/modg/bookshelf/data/metadata/GoogleBooksClientTest.kt @@ -46,6 +46,47 @@ class GoogleBooksClientTest { assertNull(client.parseResponse(body)) } + // --- classify(): the three-way per-source outcome (SPEC.md "Book metadata lookup"). --- + + @Test + fun `classify reports Found for a 2xx response with a record`() { + val result = client.classify(200, fixture("googlebooks_success.json")) + + val found = result as? SourceResult.Found + checkNotNull(found) { "expected Found, got $result" } + assertEquals("Effective Java", found.metadata.title) + } + + @Test + fun `classify reports NotFound for a 2xx response with no items`() { + val result = client.classify(200, fixture("googlebooks_no_items.json")) + assertEquals(SourceResult.NotFound, result) + } + + @Test + fun `classify reports Failed for a 404`() { + val result = client.classify(404, null) + assertEquals(SourceResult.Failed("http 404"), result) + } + + @Test + fun `classify reports Failed for a 429 keyless-quota response, distinctly from NotFound`() { + val result = client.classify(429, null) + assertEquals(SourceResult.Failed("http 429"), result) + } + + @Test + fun `classify reports Failed for a 500`() { + val result = client.classify(500, "Internal Server Error") + assertEquals(SourceResult.Failed("http 500"), result) + } + + @Test + fun `classify reports Failed for a malformed body even on a 2xx status`() { + val result = client.classify(200, fixture("malformed.json")) + assertEquals(SourceResult.Failed("malformed json"), result) + } + private fun fixture(name: String): String = checkNotNull(javaClass.classLoader.getResourceAsStream("fixtures/$name")) { "missing fixture $name" } .bufferedReader() diff --git a/app/app/src/test/java/org/modg/bookshelf/data/metadata/MetadataRepositoryTest.kt b/app/app/src/test/java/org/modg/bookshelf/data/metadata/MetadataRepositoryTest.kt index d147cc7..1555700 100644 --- a/app/app/src/test/java/org/modg/bookshelf/data/metadata/MetadataRepositoryTest.kt +++ b/app/app/src/test/java/org/modg/bookshelf/data/metadata/MetadataRepositoryTest.kt @@ -6,6 +6,80 @@ import org.junit.Test class MetadataRepositoryTest { + private val isbn13 = "9780201558029" + private val openLibraryHit = SourceResult.Found(BookMetadata(title = "Open Library Title", isbn13 = isbn13)) + private val googleBooksHit = SourceResult.Found(BookMetadata(title = "Google Books Title", isbn13 = isbn13)) + + // --- The full 3x3 (Open Library outcome x Google Books outcome) combination matrix + // from SPEC.md "Book metadata lookup": any Found wins, all-NotFound is an honest + // miss, anything else with a Failed in it is Unavailable — never a false negative. --- + + @Test + fun `Found + Found merges and returns Found`() { + val result = MetadataRepository.combine(openLibraryHit, googleBooksHit, isbn13) + assertTrue(result is LookupResult.Found) + } + + @Test + fun `Found + NotFound returns Found from the one source that had it`() { + val result = MetadataRepository.combine(openLibraryHit, SourceResult.NotFound, isbn13) + assertEquals("Open Library Title", (result as LookupResult.Found).metadata.title) + } + + @Test + fun `Found + Failed returns Found -- a reachable hit is not overruled by the other failing`() { + val result = MetadataRepository.combine(openLibraryHit, SourceResult.Failed("http 429"), isbn13) + assertEquals("Open Library Title", (result as LookupResult.Found).metadata.title) + } + + @Test + fun `NotFound + Found returns Found from the one source that had it`() { + val result = MetadataRepository.combine(SourceResult.NotFound, googleBooksHit, isbn13) + assertEquals("Google Books Title", (result as LookupResult.Found).metadata.title) + } + + @Test + fun `NotFound + NotFound returns NotFound -- both sources answered and neither had it`() { + val result = MetadataRepository.combine(SourceResult.NotFound, SourceResult.NotFound, isbn13) + assertEquals(LookupResult.NotFound, result) + } + + @Test + fun `NotFound + Failed returns Unavailable -- one honest miss is not authoritative alone`() { + val result = MetadataRepository.combine(SourceResult.NotFound, SourceResult.Failed("timeout"), isbn13) + assertTrue(result is LookupResult.Unavailable) + } + + @Test + fun `Failed + Found returns Found from the one source that had it`() { + val result = MetadataRepository.combine(SourceResult.Failed("http 500"), googleBooksHit, isbn13) + assertEquals("Google Books Title", (result as LookupResult.Found).metadata.title) + } + + @Test + fun `Failed + NotFound returns Unavailable -- one honest miss is not authoritative alone`() { + val result = MetadataRepository.combine(SourceResult.Failed("timeout"), SourceResult.NotFound, isbn13) + assertTrue(result is LookupResult.Unavailable) + } + + @Test + fun `Failed + Failed returns Unavailable and preserves both reasons`() { + val result = MetadataRepository.combine(SourceResult.Failed("http 429"), SourceResult.Failed("timeout"), isbn13) + + val unavailable = result as? LookupResult.Unavailable + checkNotNull(unavailable) { "expected Unavailable, got $result" } + assertTrue(unavailable.reason.contains("http 429")) + assertTrue(unavailable.reason.contains("timeout")) + } + + @Test + fun `a Found result without cover art falls back to the by-isbn cover url`() { + val bare = SourceResult.Found(BookMetadata(title = "No Cover", isbn13 = isbn13)) + val result = MetadataRepository.combine(bare, SourceResult.NotFound, isbn13) + + assertEquals(MetadataRepository.byIsbnCoverUrl(isbn13), (result as LookupResult.Found).metadata.coverUrl) + } + /** * `default=false` is the whole point of this URL. Without it covers.openlibrary.org * answers 200 with a 43-byte 1x1 transparent GIF for an edition it holds no art for diff --git a/app/app/src/test/java/org/modg/bookshelf/data/metadata/OpenLibraryClientTest.kt b/app/app/src/test/java/org/modg/bookshelf/data/metadata/OpenLibraryClientTest.kt index 8251309..cc73301 100644 --- a/app/app/src/test/java/org/modg/bookshelf/data/metadata/OpenLibraryClientTest.kt +++ b/app/app/src/test/java/org/modg/bookshelf/data/metadata/OpenLibraryClientTest.kt @@ -105,6 +105,47 @@ class OpenLibraryClientTest { assertNull(client.parseResponse(body, "9780201558029")) } + // --- classify(): the three-way per-source outcome (SPEC.md "Book metadata lookup"). --- + + @Test + fun `classify reports Found for a 2xx response with a record`() { + val result = client.classify(200, fixture("openlibrary_success.json"), "9780201558029") + + val found = result as? SourceResult.Found + checkNotNull(found) { "expected Found, got $result" } + assertEquals("Concrete Mathematics", found.metadata.title) + } + + @Test + fun `classify reports NotFound for a 2xx response with no record for that isbn`() { + val result = client.classify(200, fixture("openlibrary_not_found.json"), "9780201558029") + assertEquals(SourceResult.NotFound, result) + } + + @Test + fun `classify reports Failed for a 404`() { + val result = client.classify(404, null, "9780201558029") + assertEquals(SourceResult.Failed("http 404"), result) + } + + @Test + fun `classify reports Failed for a 429, distinctly from NotFound`() { + val result = client.classify(429, null, "9780201558029") + assertEquals(SourceResult.Failed("http 429"), result) + } + + @Test + fun `classify reports Failed for a 500`() { + val result = client.classify(500, "Internal Server Error", "9780201558029") + assertEquals(SourceResult.Failed("http 500"), result) + } + + @Test + fun `classify reports Failed for a malformed body even on a 2xx status`() { + val result = client.classify(200, fixture("malformed.json"), "9780201558029") + assertEquals(SourceResult.Failed("malformed json"), result) + } + private fun fixture(name: String): String = checkNotNull(javaClass.classLoader.getResourceAsStream("fixtures/$name")) { "missing fixture $name" } .bufferedReader() diff --git a/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScanCodeFilterTest.kt b/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScanCodeFilterTest.kt index 8def9dd..2e7c090 100644 --- a/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScanCodeFilterTest.kt +++ b/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScanCodeFilterTest.kt @@ -1,7 +1,6 @@ package org.modg.bookshelf.ui.scan import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull import org.junit.Test class ScanCodeFilterTest { @@ -9,45 +8,45 @@ class ScanCodeFilterTest { @Test fun `accepts a checksum-valid isbn13`() { val filter = ScanCodeFilter() - assertEquals("9780201558029", filter.accept("9780201558029")) + assertEquals(ScanOutcome.Isbn("9780201558029"), filter.accept("9780201558029")) } @Test - fun `rejects a checksum-invalid isbn13-shaped code`() { + fun `rejects a checksum-invalid isbn13-shaped code as not-an-isbn`() { val filter = ScanCodeFilter() - assertNull(filter.accept("9780201558020")) + assertEquals(ScanOutcome.NotAnIsbn("9780201558020"), filter.accept("9780201558020")) } @Test - fun `rejects non-book barcode lengths such as EAN-8 or UPC-A`() { + fun `rejects non-book barcode lengths such as EAN-8 or UPC-A as not-an-isbn`() { val filter = ScanCodeFilter() - assertNull(filter.accept("12345670")) // EAN-8 shaped - assertNull(filter.accept("012345678905")) // UPC-A shaped, 12 digits + assertEquals(ScanOutcome.NotAnIsbn("12345670"), filter.accept("12345670")) // EAN-8 shaped + assertEquals(ScanOutcome.NotAnIsbn("012345678905"), filter.accept("012345678905")) // UPC-A shaped, 12 digits } @Test - fun `rejects null raw value`() { - assertNull(ScanCodeFilter().accept(null)) + fun `ignores null raw value`() { + assertEquals(ScanOutcome.Ignored, ScanCodeFilter().accept(null)) } @Test - fun `debounces a repeat of the same code within the window`() { + fun `debounces a repeat of the same valid code within the window`() { var now = 0L val filter = ScanCodeFilter(debounceMillis = 2000L, nowMillis = { now }) - assertEquals("9780201558029", filter.accept("9780201558029")) + assertEquals(ScanOutcome.Isbn("9780201558029"), filter.accept("9780201558029")) now += 500 - assertNull(filter.accept("9780201558029")) + assertEquals(ScanOutcome.Ignored, filter.accept("9780201558029")) } @Test - fun `re-emits the same code once the debounce window elapses`() { + fun `re-emits the same valid code once the debounce window elapses`() { var now = 0L val filter = ScanCodeFilter(debounceMillis = 2000L, nowMillis = { now }) - assertEquals("9780201558029", filter.accept("9780201558029")) + assertEquals(ScanOutcome.Isbn("9780201558029"), filter.accept("9780201558029")) now += 2001 - assertEquals("9780201558029", filter.accept("9780201558029")) + assertEquals(ScanOutcome.Isbn("9780201558029"), filter.accept("9780201558029")) } @Test @@ -55,9 +54,9 @@ class ScanCodeFilterTest { var now = 0L val filter = ScanCodeFilter(debounceMillis = 2000L, nowMillis = { now }) - assertEquals("9780201558029", filter.accept("9780201558029")) + assertEquals(ScanOutcome.Isbn("9780201558029"), filter.accept("9780201558029")) now += 10 - assertEquals("9780134685991", filter.accept("9780134685991")) + assertEquals(ScanOutcome.Isbn("9780134685991"), filter.accept("9780134685991")) } @Test @@ -65,8 +64,56 @@ class ScanCodeFilterTest { var now = 0L val filter = ScanCodeFilter(debounceMillis = 2000L, nowMillis = { now }) - assertEquals("9780201558029", filter.accept("9780201558029")) + assertEquals(ScanOutcome.Isbn("9780201558029"), filter.accept("9780201558029")) filter.reset() - assertEquals("9780201558029", filter.accept("9780201558029")) + assertEquals(ScanOutcome.Isbn("9780201558029"), filter.accept("9780201558029")) + } + + // --- Rejected-code throttle (SPEC.md "Barcode scanning": a non-book barcode sitting + // in frame decodes on almost every analyzed frame, so this must not flicker). --- + + @Test + fun `a rejected code emits once, then is throttled on every subsequent frame within the window`() { + var now = 0L + val filter = ScanCodeFilter(debounceMillis = 2000L, nowMillis = { now }) + + assertEquals(ScanOutcome.NotAnIsbn("12345670"), filter.accept("12345670")) + // Simulate several more analyzed frames, all still within the debounce window. + now += 50 + assertEquals(ScanOutcome.Ignored, filter.accept("12345670")) + now += 50 + assertEquals(ScanOutcome.Ignored, filter.accept("12345670")) + now += 1000 + assertEquals(ScanOutcome.Ignored, filter.accept("12345670")) + } + + @Test + fun `a rejected code re-emits once the debounce window elapses`() { + var now = 0L + val filter = ScanCodeFilter(debounceMillis = 2000L, nowMillis = { now }) + + assertEquals(ScanOutcome.NotAnIsbn("12345670"), filter.accept("12345670")) + now += 2001 + assertEquals(ScanOutcome.NotAnIsbn("12345670"), filter.accept("12345670")) + } + + @Test + fun `a rejected code does not suppress a genuinely different valid isbn read right after it`() { + var now = 0L + val filter = ScanCodeFilter(debounceMillis = 2000L, nowMillis = { now }) + + assertEquals(ScanOutcome.NotAnIsbn("12345670"), filter.accept("12345670")) + now += 10 + assertEquals(ScanOutcome.Isbn("9780201558029"), filter.accept("9780201558029")) + } + + @Test + fun `switching between two different rejected codes does not throttle either`() { + var now = 0L + val filter = ScanCodeFilter(debounceMillis = 2000L, nowMillis = { now }) + + assertEquals(ScanOutcome.NotAnIsbn("12345670"), filter.accept("12345670")) + now += 10 + assertEquals(ScanOutcome.NotAnIsbn("012345678905"), filter.accept("012345678905")) } } diff --git a/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScanModelsTest.kt b/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScanModelsTest.kt index 5477883..5ff1ad3 100644 --- a/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScanModelsTest.kt +++ b/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScanModelsTest.kt @@ -5,6 +5,7 @@ import org.junit.Assert.assertTrue import org.junit.Test import org.modg.bookshelf.data.local.BookEntity import org.modg.bookshelf.data.metadata.BookMetadata +import org.modg.bookshelf.data.metadata.LookupResult class ScanModelsTest { @@ -25,21 +26,27 @@ class ScanModelsTest { } @Test - fun `metadata miss maps to NotFound regardless of duplicate status`() { - val outcome = ScanMetadataOutcome.from("9780201558029", null, DuplicateStatus.New) + fun `an authoritative NotFound result maps to NotFound regardless of duplicate status`() { + val outcome = ScanMetadataOutcome.from("9780201558029", LookupResult.NotFound, DuplicateStatus.New) assertEquals(ScanSheetState.NotFound("9780201558029"), outcome) } @Test - fun `metadata hit maps to Found carrying the duplicate status through`() { + fun `a Found result maps to Found carrying the duplicate status through`() { val metadata = BookMetadata(title = "Dune", isbn13 = "9780201558029") val duplicate = DuplicateStatus.AlreadyOwned("abc123", "Dune") - val outcome = ScanMetadataOutcome.from("9780201558029", metadata, duplicate) + val outcome = ScanMetadataOutcome.from("9780201558029", LookupResult.Found(metadata), duplicate) assertEquals(ScanSheetState.Found("9780201558029", metadata, duplicate), outcome) } + @Test + fun `an Unavailable result maps to LookupFailed and never claims the book is unknown`() { + val outcome = ScanMetadataOutcome.from("9780201558029", LookupResult.Unavailable("http 429"), DuplicateStatus.New) + assertEquals(ScanSheetState.LookupFailed("9780201558029", "http 429"), outcome) + } + @Test fun `session count starts at zero and increments per save, not per skip`() { var session = ScanSessionState() diff --git a/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScannerControllerTest.kt b/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScannerControllerTest.kt index ff1ca2e..6daaa02 100644 --- a/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScannerControllerTest.kt +++ b/app/app/src/test/java/org/modg/bookshelf/ui/scan/ScannerControllerTest.kt @@ -46,6 +46,39 @@ class ScannerControllerTest { assertEquals(listOf("9780201558029"), received) } + @Test + fun `non-book barcode emits on rejectedCodes, not scanResults`() = runTest { + val controller = ScannerController() + val scanned = mutableListOf() + val rejected = mutableListOf() + backgroundScope.launch { controller.scanResults.toList(scanned) } + backgroundScope.launch { controller.rejectedCodes.toList(rejected) } + runCurrent() + + controller.onBarcodeScanned("012345678905") // UPC-A shaped, fails the ISBN-13 checksum + runCurrent() + + assertTrue(scanned.isEmpty()) + assertEquals(listOf("012345678905"), rejected) + } + + @Test + fun `a debounced repeat reaches neither scanResults nor rejectedCodes`() = runTest { + val controller = ScannerController() + val scanned = mutableListOf() + val rejected = mutableListOf() + backgroundScope.launch { controller.scanResults.toList(scanned) } + backgroundScope.launch { controller.rejectedCodes.toList(rejected) } + runCurrent() + + controller.onBarcodeScanned("012345678905") + controller.onBarcodeScanned("012345678905") // same frame's worth of repeat reads + runCurrent() + + assertEquals(listOf("012345678905"), rejected) + assertTrue(scanned.isEmpty()) + } + @Test fun `torch toggles from its default off state`() { val controller = ScannerController() diff --git a/app/app/src/test/java/org/modg/bookshelf/ui/screens/ScanScreenPaparazziTest.kt b/app/app/src/test/java/org/modg/bookshelf/ui/screens/ScanScreenPaparazziTest.kt index b88df94..eaa8f0f 100644 --- a/app/app/src/test/java/org/modg/bookshelf/ui/screens/ScanScreenPaparazziTest.kt +++ b/app/app/src/test/java/org/modg/bookshelf/ui/screens/ScanScreenPaparazziTest.kt @@ -27,6 +27,8 @@ import org.modg.bookshelf.data.metadata.BookMetadata import org.modg.bookshelf.ui.components.BookshelfScaffold import org.modg.bookshelf.ui.scan.DuplicateStatus import org.modg.bookshelf.ui.scan.FoundBookSheet +import org.modg.bookshelf.ui.scan.LookupFailedSheet +import org.modg.bookshelf.ui.scan.RejectedBarcodeBanner import org.modg.bookshelf.ui.scan.ScanReticle import org.modg.bookshelf.ui.scan.SearchingSheet import org.modg.bookshelf.ui.scan.SessionBadge @@ -57,6 +59,14 @@ class ScanScreenPaparazziTest { @Test fun scanSearchingSheetLight() = snapshotBoth("scan-searching-sheet") { SearchingSheetOverlay() } + /** Neither source could be reached — see [LookupFailedSheet]. Must not read as "not found". */ + @Test + fun scanLookupFailedSheetLight() = snapshotBoth("scan-lookup-failed-sheet") { LookupFailedSheetOverlay() } + + /** A decoded barcode that failed the ISBN-13 checksum — see [RejectedBarcodeBanner]. */ + @Test + fun scanRejectedBarcodeLight() = snapshotBoth("scan-rejected-barcode") { RejectedBarcodeOverlay() } + @Composable private fun ReticleOverlay() = Shell { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -107,6 +117,35 @@ class ScanScreenPaparazziTest { } } + @Composable + private fun LookupFailedSheetOverlay() = Shell { + Box(modifier = Modifier.fillMaxSize()) { + Surface( + modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth(), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + color = MaterialTheme.colorScheme.surface, + ) { + LookupFailedSheet( + isbn13 = "9780765326355", + onRetry = {}, + onEnterByHand = {}, + onSkip = {}, + ) + } + } + } + + @Composable + private fun RejectedBarcodeOverlay() = Shell { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + ScanReticle(modifier = Modifier.align(Alignment.Center)) + RejectedBarcodeBanner( + message = "Read 012345678905 — not a book barcode", + modifier = Modifier.align(Alignment.BottomCenter).padding(24.dp), + ) + } + } + @Composable private fun Shell(overlay: @Composable () -> Unit) { BookshelfScaffold( diff --git a/app/app/src/test/snapshots/images/org.modg.bookshelf.ui.screens_ScanScreenPaparazziTest_scanLookupFailedSheetLight_scan-lookup-failed-sheet-dark.png b/app/app/src/test/snapshots/images/org.modg.bookshelf.ui.screens_ScanScreenPaparazziTest_scanLookupFailedSheetLight_scan-lookup-failed-sheet-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a1dd69ed0aad02bcbf65e43c4a5649259d1dc71a GIT binary patch literal 28681 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 ipEyD}uOu}8h1