From 9e06e1f3e9706763ca20f308e46bdcb6f2d455a2 Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 5 Nov 2026 11:55:19 -0600 Subject: [PATCH] test(scanner): verify immediate photo availability and preview Assert the Echo fixture's available captures and generic hosts' quarantine behavior. Scope thumbnail and preview checks to the newly uploaded photo. Update release and installation guidance with signed MC9400 qualification and the remaining native authentication and publication gates. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: James Maes --- README.md | 2 +- docs/INSTALLATION.md | 18 +++++-- docs/RECORD-UX-2026-11-05.md | 4 +- docs/RELEASING.md | 42 ++++++++++++--- docs/SESSION-STATE.md | 20 ++++++-- docs/TODO.md | 16 +++--- .../mobile/scanner/ScannerEndToEndTest.kt | 26 +++++++--- .../scanner/PhotoUploadViewModelTest.kt | 51 ++++++++++++++++++- 8 files changed, 148 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 24cc5a4..4c95baf 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ See [emulator testing](docs/EMULATOR.md) for an emulator demo, synthetic scans a ### Signed APK (direct install / MDM) -For real devices. Requires an **HTTPS** server URL and the production-signed APK built with the QRun-owned key. Follow the [installation guide](docs/INSTALLATION.md) and [release readiness](docs/RELEASING.md). A signed 1.0.0 (versionCode 2) APK is published as a [signed preview release](https://git.kof22.com/QRun-IO/qqq-android/releases/tag/scanner-v1.0.0) (prerelease, forge sign-in required). It was installed and launched on one MC9400, but production HTTPS enrollment, physical scanning and in-place update have not been qualified, so it is not a stable release. The development-signed and production-signed builds cannot update each other in place. +Requires an **HTTPS** server URL and the production-signed APK built with the QRun-owned key. Follow the [installation guide](docs/INSTALLATION.md) and [release readiness](docs/RELEASING.md). The published download remains the [signed 1.0.0 preview](https://git.kof22.com/QRun-IO/qqq-android/releases/tag/scanner-v1.0.0) (prerelease, forge sign-in required). The signed 1.0.1 candidate has passed trusted-HTTPS enrollment, physical scanning, navigation and an in-place update on the MC9400, preserving login and enrollment. Its publication, native Authentik qualification and final dependency-cohort checks remain open. Development-signed installations require an explicit migration to the production signer. ## Build and test diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index dea8eae..3096137 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -4,6 +4,16 @@ Use this for the **production-signed** APK on a small number of devices. Move to This guide is not the local demo. The demo uses the development-signed debug APK, plain HTTP and `adb reverse` port forwarding to a workstation server; see [README](../README.md#installing-on-a-device). The two builds have different signers and cannot update each other. +**Current release status (November 5, 2026):** the only published release is the signed preview 1.0.0 below. A signed **1.0.1 (versionCode 3) candidate** exists but is **not published and has no download link**; this guide does not point to one. Use the table below for what is actually hosted. When 1.0.1 is published, use the file, checksum and link given by the release owner, and verify the same signer. + +| Candidate (not published) | Value | +| --- | --- | +| Version | 1.0.1, versionCode 3 | +| APK SHA-256 | `5124716B59CAD303717E7834FE64DDC1537E33A25C9AB75A6EA1ACF03AC2717F` (15015419 bytes) | +| Signing certificate SHA-256 | Same as below (`BDDB2D2F...5B17A9FF`) | + +Details and qualification status: [RELEASING](RELEASING.md). + | Item | Value | | --- | --- | | Source | [Signed preview release scanner-v1.0.0](https://git.kof22.com/QRun-IO/qqq-android/releases/tag/scanner-v1.0.0) (prerelease; forge sign-in required) | @@ -15,12 +25,12 @@ This guide is not the local demo. The demo uses the development-signed debug APK The same APK is used for every customer; branding comes from the server at runtime. Download the APK and `SHA256SUMS` from the release page and compare the SHA-256 before installing, then verify the signing certificate matches the value above. The checksum shows the file is intact, not who signed it. This is a preview, not a stable release. Only the APK and `SHA256SUMS` are hosted; there is no hosted install kit. -This APK was installed and launched once on a fresh Zebra MC9400 (Android 13): install succeeded, the Connect screen appeared and the installed bytes matched the hash above. It has **not** been enrolled, scanned or navigated, because no HTTPS server was available, so these steps are not hardware-qualified until the first install is confirmed (below). That MC9400 now runs the development demo build instead. +**Device evidence.** *Historical (November 4):* this APK was installed and launched once on a fresh Zebra MC9400 (Android 13) and showed the Connect screen; it was not yet enrolled. *Current (November 5):* with the user's explicit authorization, the MC9400 was migrated: 1.0.0 enrollment over trusted HTTPS passed, then an in-place 1.0.1 update preserved login and enrollment. Apps/ScanCapture navigation and EMDK Scanner-ready were observed, and the operator confirmed a physical Saved scan that the backend verified. These checks are done and need not be repeated for this device. The 1.0.1 candidate is still unpublished, native Authentik login/logout/account switching is not yet qualified with the production APK, and the TC22 keeps its original development-signed installation. ## Before you start - The device is connected to Wi-Fi. -- You have an **HTTPS** QQQ server URL the device can reach. Release builds reject HTTP. The server is not chosen yet: use the address you are given, and do not guess one. +- You have a customer **HTTPS** QQQ server URL with a trusted certificate that the device can reach. Release builds reject HTTP, and certificate checks must not be weakened. Use the address you are given (`https://`); do not guess one. Local-network qualification used a temporary private network and is not a customer deployment. - You have the QQQ credentials for the sign-in the server offers. None are in this guide. ## Install: copy the file (recommended) @@ -52,7 +62,7 @@ Grant location when Android asks; scanning works without it. Zebra devices with ## Confirm the first new install -On the first device only, check each of these: +On the first device of a new customer deployment, check each of these (already done for the MC9400 qualification above): - The footer shows **Connected**. - One scan is captured and the server keeps it (**Saved scan**). @@ -67,7 +77,7 @@ For a later build signed by the same key with a higher versionCode, run the same ## Devices running the development-signed build -Devices running the development-signed demo build (the TC22, and the MC9400 in demo mode) cannot be updated by this APK, because the signer differs. Android is expected to refuse the install. This is not a fault: do not uninstall the app or reset the device to get around it. Moving that device needs a planned one-time migration that includes re-enrollment. +Devices running the development-signed demo build (the TC22) cannot be updated by this APK, because the signer differs. Android is expected to refuse the install. This is not a fault: do not uninstall the app or reset the device to get around it. Moving that device needs an explicitly authorized, planned one-time migration that includes re-enrollment. ## References diff --git a/docs/RECORD-UX-2026-11-05.md b/docs/RECORD-UX-2026-11-05.md index 8df637e..9a0073a 100644 --- a/docs/RECORD-UX-2026-11-05.md +++ b/docs/RECORD-UX-2026-11-05.md @@ -8,7 +8,9 @@ Source review: [Android PR 15](https://git.kof22.com/QRun-IO/qqq-android/pulls/1 Open **Test Data → Test Orders**, search or filter, and select a record. Back preserves page/search/filter context. A fixed identity heading sits above Details, Items, History and Photos. Groups collapse and can be selected from Sections. Related items show concise reference/quantity/SKU labels; timestamps include timezone context and server currency/units are preserved. -Use **Actions → Add photo → Take photo / Choose photo**. The selected record remains visible in the chooser. Saved attachments appear under Photos with review status. Available images have thumbnails and previews; quarantined originals remain unavailable. Photo transfer remains online-only, idempotent and protected by parent permissions and device/session authorization. +Use **Actions → Add photo → Take photo / Choose photo**. The selected record remains visible in the chooser. Saved attachments appear under Photos with review status. Available images have thumbnails and previews; quarantined originals remain unavailable. The client follows the server's receipt: a host that admits approved-device captures immediately returns `AVAILABLE`, so the app shows "Saved photo."; hosts that retain quarantine return `QUARANTINED` ("Awaiting server review."). The maintained Echo host now uses Record Files' scoped `quarantineCapturedFiles` setting. The app has no admission policy of its own and no client change was needed. Photos already uploaded stay quarantined until the server releases them; the three earlier demo captures were released through the normal audited admin process. Photo transfer remains online-only, idempotent and protected by parent permissions and device/session authorization. + +The signed MC9400 app's gallery upload returned "Saved photo." for a new synthetic PNG; server readback verified `AVAILABLE`, the intended parent, media type, byte count and SHA256. Updated native acceptance on the TC22 also passed synthetic camera/gallery uploads, the newly captured photo's thumbnail and full preview, parent scoping, rotation and forced-logout cleanup. All requests passed semantic API checks; the disposable app was removed afterward. This is not a camera-hardware qualification. The captured-photo policy still needs its public dependency release. ## Verification diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 61e09e1..53e1f7c 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,6 +1,18 @@ # Scanner APK release readiness -Status (November 4, 2026): **a QRun-owned production signing key exists with a verified backup, and the signed 1.0.0 (versionCode 2) APK is published as a signed preview (Gitea prerelease, not a stable release): [scanner-v1.0.0](https://git.kof22.com/QRun-IO/qqq-android/releases/tag/scanner-v1.0.0), release ID 1361, tag on protected `main` `3eef6431ca39b6450cf764424d921a02b2afb9ab`. It was installed and launched once on a fresh MC9400, but production HTTPS enrollment, physical scanning and in-place update qualification are still pending.** The owner selected direct signed-APK / MDM distribution. Current local demo devices (TC22 and, by the user's later choice, the MC9400) run the development-signed debug APK (0.1.0, versionCode 1) against the local HTTP server; that is not a production release. Manual TC22 acceptance was reported all-pass by the operator on November 4 (operator report, no per-case artifacts) for the development-signed build; it does not qualify the production signer and is not requested again for that build. +## Current status (November 5, 2026) + +- **Published:** the only published release is still [scanner-v1.0.0](https://git.kof22.com/QRun-IO/qqq-android/releases/tag/scanner-v1.0.0), a **signed preview** (Gitea prerelease, not a stable release). It is unchanged. +- **Candidate, not published:** QQQ Mobile **1.0.1 (versionCode 3)** is a signed candidate. Its release qualification is incomplete, and no tag or download link is available yet. +- **Signer:** the candidate is signed with the existing permanent QRun key (certificate SHA-256 `BDDB2D2F45A5925C643A83ABAF616C1EAB2387DC5CDA7B645A6F4C0D5B17A9FF`). No new or replacement key was created; the existing recovery backup was verified earlier. +- **Candidate APK:** SHA-256 `5124716B59CAD303717E7834FE64DDC1537E33A25C9AB75A6EA1ACF03AC2717F`, 15015419 bytes. +- **Source:** Android PR 15 `ab6e44c`, merged as `c782aaaaba3f62422232436a9000af9d4b38fc82` with the same app-source tree. Installed APK bytes, signature, non-debuggable flag and 16 KiB alignment were verified. `lintRelease` reported 0 errors, 0 warnings and 5 informational findings; the release build passed. +- **Distribution:** maintainers use the existing manual signing path for direct APK/MDM distribution. Automated signing CI is optional. +- **Open:** see [Qualification status](#qualification-status) and [TODO](TODO.md). + +## Historical status (November 4, 2026, superseded where noted) + +Dated evidence below is preserved. As of November 4: a QRun-owned production signing key existed with a verified backup, and the signed 1.0.0 (versionCode 2) APK was published as a signed preview: [scanner-v1.0.0](https://git.kof22.com/QRun-IO/qqq-android/releases/tag/scanner-v1.0.0), release ID 1361, tag on protected `main` `3eef6431ca39b6450cf764424d921a02b2afb9ab`. It had been installed and launched once on a fresh MC9400; at that time enrollment, scanning and update were unqualified. *(Superseded November 5: the MC9400 was migrated to the signed build; see below. The statement that the MC9400 ran the development-signed debug APK is no longer current. The TC22 still runs its original development-signed installation.)* Manual TC22 acceptance was reported all-pass by the operator on November 4 (operator report, no per-case artifacts) for the development-signed build; it does not qualify the production signer and is not rerun. ## Decisions (made) @@ -34,12 +46,26 @@ Version `1.0.0`, `versionCode` 2 was built unsigned (`lintRelease` and `assemble **Device evidence (MC9400, November 4):** the signed APK was installed on a fresh MC9400 (Android 13, EMDK present), launched without a startup crash, showed the Connect screen, and its installed bytes read back with the exact hash above. It was not enrolled: release builds require HTTPS and the only server available was the local HTTP demo. The same key and signer are unchanged. The user then chose to run the MC9400 in demo mode like the TC22, so that fresh, unenrolled production install was replaced with the development debug APK and port forwarding to the local server. No signed-build enrollment, scan or navigation was tested. The TC22 and its data were untouched throughout. -Remaining steps: +*(The November 4 MC9400 paragraph above is historical: the fresh install was later replaced by the migration recorded below.)* -1. Qualify the **actual production-signed APK** on a device: install, enrollment, scanner path ([Zebra scanning](ZEBRA-SCANNING.md)), Back/Apps navigation, and update over a prior production build. Use a planned device migration or a spare device; do not use a demo device implicitly, and note that moving a demo device to the signed build removes its development install and enrollment. -2. Provide an **HTTPS** server. Source release builds disallow cleartext; HTTP is limited to debug builds. The local demo does not need HTTPS. -3. Use the first direct installation to confirm the device flow, then distribute the same APK to the remaining devices. MDM can be introduced later. -4. Automated scanner-APK signing/release workflow is not configured and is separate from manual distribution. The preview release exists; create no further tags or releases (including a stable promotion) without current authorization. A stable release needs the qualification in step 1. +## Qualification status + +Done (not to be repeated as unchecked work): + +- **MC9400 migration:** the user explicitly authorized it. Release 1.0.0 enrollment over trusted HTTPS passed, then an in-place 1.0.1 update preserved login and enrollment. Apps/ScanCapture navigation and EMDK Scanner-ready state were observed. +- **Physical scan:** the operator confirmed a physical Saved scan. The backend verified CODE128, AIM identifier, raw bytes, user and device. The actual barcode differed from the requested literal. +- **TC22:** the original development-signed installation and its data are retained. The accepted November 4 manual all-pass is not rerun. +- **Server discovery:** against the real shared Authentik-backed server, UI API 1.10 discovery with strict nonce passed for both scanner and printing channels, extensions and callbacks. +- **Photo policy (live):** a signed MC9400 gallery upload returned Saved photo with `AVAILABLE` status and exact parent, type, size and hash; the previous three photos were released normally. Updated native acceptance on the TC22 passed synthetic camera/gallery uploads, the captured photo's thumbnail and full preview, parent scoping, rotation and forced-logout cleanup. All requests passed the semantic API checks. The disposable app was removed and the normal app reopened. This does not establish camera-hardware or native-Authentik acceptance. +- **Local network:** the local QQQ server remains on a PC and was reached over trusted HTTPS temporarily through a user-approved private Tailscale network. This is local qualification, not a customer production deployment. Customer instructions still require a customer HTTPS URL with a trusted certificate. TLS must not be weakened, and workstation hostnames, IPs, ports, device IDs, vault IDs, private file paths and passwords are not published. + +Open: + +1. **Native Authentik** login, logout and account switching with the actual production APK remain open, awaiting the MC wireless-debug endpoint. No native-Authentik PASS is claimed. +2. **Printing:** the print nonce fix (PR 11, merged `abc3d0d`, 532 hosted tests passed) postdates the published print 1.1.1; the next installer remains with the active printing owner. +3. Public compatible Core/Collab/document/files/scripting/bridges, the final public cohort gate, GS1 physical print/rescan, and full release-asset publication and distribution. +4. Company profiles and wallpaper are tracked separately. +5. Publish the 1.0.1 tag, release, APK, checksum and installation instructions after its remaining qualification. The published download remains the 1.0.0 signed preview; automated signing CI is optional. ## Release requirements @@ -51,8 +77,8 @@ Remaining steps: Android updates in place only when the signer matches and `versionCode` does not decrease. Losing this key, or its backup, means installed copies can no longer be updated in place; they would have to be uninstalled and reinstalled, which removes enrollment and session state. Keep the vault backup recoverable and re-verify it (hash, open, certificate comparison) after any change to it. -Development-signed demo installations (TC22, and the MC9400 in demo mode) cannot be upgraded by the production key. Moving one requires an explicit migration: preserve its test evidence, plan re-enrollment, and never uninstall or reset a device or application implicitly. No update or key-rotation policy beyond keeping this one permanent signer is defined here. +Development-signed demo installations (the TC22) cannot be upgraded by the production key. Moving one requires an explicit migration: preserve its test evidence, plan re-enrollment, and never uninstall or reset a device or application implicitly. The MC9400 was migrated under explicit user authorization (see above). No update or key-rotation policy beyond keeping this one permanent signer is defined here. ## Not yet established -Production-signed-APK device qualification (the preview is published, but production HTTPS enrollment, scanning and in-place update are unqualified), stable-release promotion, an HTTPS production server, an automated scanner release workflow and its secret handling, and key-rotation planning. SDK publication by the central workflow is done and its completed audits are not reopened here. Other pending items are in [TODO](TODO.md). +Publication of the 1.0.1 candidate, native Authentik qualification with the production APK, a customer production HTTPS deployment, stable-release promotion, an automated scanner release workflow (optional) and its secret handling, and key-rotation planning. SDK publication by the central workflow is done and its completed audits are not reopened here. Other pending items are in [TODO](TODO.md). diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index d8d368e..c1eaf76 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -1,12 +1,24 @@ # Current status -As of **2026-11-05**. Scope: scanner host and maintained local Echo workflow. [TODO](TODO.md) tracks remaining work; [history](history/README.md) preserves earlier evidence. This is a development preview, not production 1.0 qualification. +As of **2026-11-05**. Scope: scanner host and maintained local Echo workflow. [TODO](TODO.md) tracks remaining work; [history](history/README.md) preserves earlier evidence. This is not a final-release or full-qualification claim. See [Release facts](#release-facts-2026-11-05) below. + +## Release facts (2026-11-05) + +- **Published:** still `scanner-v1.0.0`, a signed preview. Signed candidate **1.0.1 (versionCode 3)** is **not published**, has no tag and no download link, and is not production-complete. Signer: existing permanent QRun key (certificate SHA-256 `BDDB2D2F...5B17A9FF`); no new key. APK SHA-256 `5124716B59CAD303717E7834FE64DDC1537E33A25C9AB75A6EA1ACF03AC2717F`, 15015419 bytes. Details: [RELEASING](RELEASING.md). +- **Source:** Android PR 15 `ab6e44c`, merged `c782aaaaba3f62422232436a9000af9d4b38fc82`, same app-source tree. Root verified installed APK bytes/signature/non-debuggable/16 KiB alignment; `lintRelease` 0 errors, 0 warnings, 5 informational; release build passed. +- **Done:** MC9400 migration (user-authorized): 1.0.0 enrollment over trusted HTTPS, then in-place 1.0.1 update preserving login/enrollment; Apps/ScanCapture and EMDK Scanner-ready observed; operator-confirmed physical Saved scan verified in the backend (CODE128/AIM/raw bytes/user/device; actual barcode differed from the requested literal). Not repeat work. TC22 keeps its original development-signed installation and data; the accepted November 4 manual all-pass is not rerun. +- **Network:** the local QQQ server stays on a PC, reached over trusted HTTPS temporarily through a user-approved private Tailscale network. This is not a customer production deployment; customer instructions require a customer HTTPS URL with a trusted certificate. TLS is not weakened; no hostnames, IPs, ports, device/vault IDs, private paths or passwords are published. +- **Server/Authentik:** real shared Authentik server discovery verified (UI API 1.10, strict nonce, scanner and printing channels/extensions/callbacks). Production-APK native login/logout/account switching is **open**, awaiting the MC wireless-debug endpoint; no native-Authentik PASS is claimed. Print nonce fix PR 11 (merged `abc3d0d`, 532 hosted tests passed) postdates published print 1.1.1; the next installer remains with the active printing owner. +- **Photo policy:** live. A signed MC9400 gallery synthetic PNG became a Saved photo, `AVAILABLE`, with exact parent/type/size/hash; the previous three photos were released normally. The app has no admission policy; the server receipt controls the message. The root is running the updated disposable TC22 E2E; no full new-preview or native PASS is claimed until it completes. +- **Open:** public compatible Core/Collab/document/files/scripting/bridges, final public cohort gate, GS1 physical print/rescan, full release-asset publication/distribution. Company profiles/wallpaper are tracked separately; signing CI is optional and not blocking a direct APK release. Release authorizations already exist. + +Statements below were written before these facts and are historical where they conflict (for example "debug update", MC9400 trigger check "outstanding", signed-build HTTPS qualification "open"). ## Record browsing and photos The existing TC22 app now opens permitted tables and records through the standard semantic metadata/query/get API. Tables support search, exact text filters, counts and stable paging; Back restores page/search/filter context. Records retain identity above Details, related-record and Photos tabs, with collapsible groups and a section selector. Dates/timezones, currency and empty values are readable; identifiers keep leading zeros. -**Record → Actions → Add photo** opens Take photo / Choose photo. Saved photos show parent-scoped lifecycle status and approved thumbnails/previews. Quarantined originals remain unavailable. Existing upload identity, retry handling, server authorization, pending-navigation guards and logout cleanup are preserved. +**Record → Actions → Add photo** opens Take photo / Choose photo. Saved photos show parent-scoped lifecycle status and approved thumbnails/previews. Quarantined originals remain unavailable. Availability is decided only by the server receipt/lifecycle; focused tests cover both `AVAILABLE` and `QUARANTINED`, and the native E2E expects `AVAILABLE` for the configured Echo fixture while retaining quarantine assertions for the generic fixture. Thirteen focused photo/API tests passed. A fresh synthetic gallery upload through the signed MC9400 app returned "Saved photo."; server readback verified availability, parent, type, byte count and SHA256. The three earlier demo captures were released through the normal audited admin process. Updated TC22 native photo acceptance passed synthetic camera/gallery upload, the captured photo thumbnail/full preview, parent scoping, rotation and forced-logout cleanup, with all requests checked against the semantic API. The disposable test packages were removed and the normal app reopened. Public captured-photo dependency publication and native Authentik qualification remain open. Existing upload identity, retry handling, server authorization, pending-navigation guards and logout cleanup are preserved. The maintained companion demo supplies Test Data: 1,000 orders, 39 fields in six groups, 12,000 items and 3,000 history entries. See [screenshots and verification](RECORD-UX-2026-11-05.md). @@ -22,8 +34,8 @@ The maintained companion demo supplies Test Data: 1,000 orders, 39 fields in six Protected `main` contains PRs 8/10, with signing/documentation through PR 14 (`464814f`). The user authorized commit, push and merge of the GS1/record/UX work through normal Gitea checks. No new artifact release was requested. The maintained server uses public QQQ RC.21 and Printing 1.0.0 plus qualified document/files/scripting owner inputs. Contract versions remain UI API 1.10, mobile-scanners 1.5, printing 2.3 and mobile-record-files 1.0. Device loopback 8080 forwards to the existing host port 57832. -The QRun-owned signing key and [signed preview `scanner-v1.0.0`](https://git.kof22.com/QRun-IO/qqq-android/releases/tag/scanner-v1.0.0) remain unchanged. That preview predates this debug update. It is not a stable release or an automatic signing/publication pipeline; see [RELEASING](RELEASING.md). +The QRun-owned signing key and [signed preview `scanner-v1.0.0`](https://git.kof22.com/QRun-IO/qqq-android/releases/tag/scanner-v1.0.0) remain unchanged. That preview predates the record UX update; candidate 1.0.1 is unpublished. The preview is not a stable release or an automatic signing/publication pipeline; see [RELEASING](RELEASING.md). ## Accepted and outstanding hardware work -The November 4 TC22 operator group pass remains accepted; do not request it again. MC9400 remains installed, signed in and approved, with a physical trigger/backend check outstanding. Physical GS1 print/rescan, native provider callback, signed-build HTTPS qualification, public owner-dependency adoption and fleet/MDM rollout remain open. Historical hashes, signed-preview provenance and older tests are preserved in [the prior checkpoint](history/SESSION-STATE-before-record-ux-2026-11-05.md). +The November 4 TC22 operator group pass remains accepted; do not request it again. MC9400's physical scan/backend check and signed-build trusted-HTTPS enrollment/update are done (see Release facts). Physical GS1 print/rescan, native provider callback, public owner-dependency adoption and fleet/MDM rollout remain open. Historical hashes, signed-preview provenance and older tests are preserved in [the prior checkpoint](history/SESSION-STATE-before-record-ux-2026-11-05.md). diff --git a/docs/TODO.md b/docs/TODO.md index 26be251..019cb29 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,15 +5,19 @@ Open work only, as of **2026-11-05**. Current evidence: [SESSION-STATE](SESSION- ## Hardware and release qualification - [ ] Qualify actual GS1 label media and physical printed-label rescan. Synthetic capture/render evidence does not close this check. -- [ ] Confirm one physical MC9400 trigger scan in the backend. The normal app is installed, signed in and approved. -- [ ] Qualify the production-signed build against an HTTPS server: enrollment, scanner path, navigation and update. -- [ ] Verify an actual native Authentik/provider callback, logout and account switching. -- [ ] Distribute the qualified signed build to remaining devices; validate MDM/company profile separately. The published signed preview predates the current record UX update. -- [ ] Automatic APK signing/publication remains owned by the shared CI design; no new pipeline is configured here. +- [ ] Verify actual production-APK native Authentik login, logout and account switching; awaiting the MC wireless-debug endpoint. Real-server discovery (UI API 1.10, strict nonce, scanner and printing channels) is already verified; no native-Authentik PASS is claimed. +- [ ] Publish signed candidate 1.0.1 (versionCode 3, APK SHA-256 `5124716B...AC2717F`, same permanent signer), checksum and installation instructions after remaining qualification. The published release is still the 1.0.0 signed preview. +- [ ] Publish and distribute full release assets to remaining devices; validate MDM/company profile and wallpaper separately. A customer deployment needs a customer HTTPS URL with a trusted certificate. +- [ ] Printing: the next installer remains with the active printing owner (print nonce fix PR 11 postdates published print 1.1.1). +- [ ] Optional, non-blocking: automatic APK signing CI. Direct APK release does not depend on it. + +Done (November 5, do not repeat): MC9400 migration with release 1.0.0 trusted-HTTPS enrollment then in-place 1.0.1 update preserving login/enrollment; Apps/ScanCapture navigation and EMDK Scanner-ready observed; operator-confirmed physical Saved scan verified in the backend (CODE128, AIM, raw bytes, user, device; actual barcode differed from the requested literal). Live photo policy verified with a signed MC9400 synthetic gallery photo (`AVAILABLE`, exact parent/type/size/hash). + +Updated TC22 native photo acceptance passed: synthetic camera/gallery uploads, the newly captured photo's thumbnail/full preview, parent scoping, rotation and forced-logout cleanup, with semantic API checks. The disposable test packages were removed and the normal app reopened. Native Authentik remains a separate open check. ## Dependencies and broader coverage -- [ ] Adopt public, coherent Generation/Record Files/Scripting owner inputs through a reviewed companion-server change and a fresh complete gate. +- [ ] Adopt public compatible Core/Collab/document/files/scripting/bridges inputs through a reviewed companion-server change, and pass the final public cohort gate. - [ ] Verify company profile / EHS / OEMConfig / MDM deployment ([DEVICE-PROVISIONING](DEVICE-PROVISIONING.md), [COMPANY-PROFILE](COMPANY-PROFILE.md)). - [ ] Resolve applicable [legacy source follow-ups](LEGACY-SOURCE-FOLLOWUPS.md). - [ ] Extend specialized forms, reports and other unsupported [native components](UI-API.md) with conformance and E2E coverage as needed. Read-only table/record browsing and record photo attachment are implemented. diff --git a/scanner-host/src/androidTest/java/io/qrun/mobile/scanner/ScannerEndToEndTest.kt b/scanner-host/src/androidTest/java/io/qrun/mobile/scanner/ScannerEndToEndTest.kt index ca8a269..3f8038d 100644 --- a/scanner-host/src/androidTest/java/io/qrun/mobile/scanner/ScannerEndToEndTest.kt +++ b/scanner-host/src/androidTest/java/io/qrun/mobile/scanner/ScannerEndToEndTest.kt @@ -401,6 +401,14 @@ class ScannerEndToEndTest { val config = JSONObject(configFile.readText()) configFile.delete() origin = config.getString("serverUrl").trimEnd('/') + // Explicit fixture expectations, not production host classification: the rich-records scenario (rich=true) runs + // against the configured EchoDemo host, which admits an approved device's captures immediately + // (Record Files quarantineCapturedFiles=false) and so expects AVAILABLE. The generic document/process photo + // fixtures (rich=false) expect QUARANTINED. + val echoHost = rich + val savedMessage = if (echoHost) "Saved photo." else "Saved photo. Awaiting server review." + val expectedLifecycle = if (echoHost) "AVAILABLE" else "QUARANTINED" + val expectedBadge = if (echoHost) "Available" else "Awaiting review" val bitmap = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.BLUE) } val bytes = ByteArrayOutputStream().use { output -> bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output); output.toByteArray() } bitmap.recycle() @@ -513,7 +521,8 @@ class ScannerEndToEndTest { } waitFor("Take photo") ui.onNodeWithText("Take photo").performScrollTo().performClick() - waitFor("Saved photo. Awaiting server review.") + waitFor(savedMessage) + if (echoHost) ui.onNodeWithText("Saved photo. Awaiting server review.").assertDoesNotExist() assertEquals("Camera result must automatically upload without a second tap", 1, cameraCalls.get()) assertTrue(directory.listFiles().orEmpty().none { it.name.startsWith("photo-") }) val savedRows = jsonRequest("/qqq/v1/table/record_files_file/query", JSONObject()).getJSONArray("records") @@ -523,7 +532,7 @@ class ScannerEndToEndTest { assertEquals(parentId, savedFile.getString("recordId")) assertEquals("CAPTURE", savedFile.getString("origin")) assertTrue(savedFile.getString("originDetail").endsWith("source CAMERA")) - assertEquals("QUARANTINED", savedFile.getString("lifecycle")) + assertEquals(expectedLifecycle, savedFile.getString("lifecycle")) assertEquals(bytes.size.toLong(), savedFile.getLong("sizeBytes")) assertTrue(savedFile.getLong("attachmentId") > 0) val associations = jsonRequest("/qqq/v1/table/record_files_collection_item/query", JSONObject()).getJSONArray("records") @@ -536,13 +545,14 @@ class ScannerEndToEndTest { waitFor("Photo ready to upload.") assertEquals(1, galleryCalls.get()) ui.onNodeWithText("Upload photo").performScrollTo().performClick() - waitFor("Saved photo. Awaiting server review.") + waitFor(savedMessage) val galleryRows = jsonRequest("/qqq/v1/table/record_files_file/query", JSONObject()).getJSONArray("records") val gallerySaved = (0 until galleryRows.length()).map { galleryRows.getJSONObject(it).getJSONObject("values") } .single { it.optString("deviceId") == device.deviceId && it.optString("originDetail").endsWith("source GALLERY") } assertEquals(parentTable, gallerySaved.getString("tableName")) assertEquals(parentId, gallerySaved.getString("recordId")) assertEquals(RecordFilesApi.sha256(bytes), gallerySaved.getString("sha256")) + assertEquals(expectedLifecycle, gallerySaved.getString("lifecycle")) assertTrue(gallerySaved.getLong("attachmentId") > 0) stage("native-photo-gallery-saved") if (fromRecord) { @@ -551,8 +561,12 @@ class ScannerEndToEndTest { waitFor(if (rich) "3 photos" else "2 photos") if (rich) { ui.onNodeWithText("Preview example 27.jpg").assertDoesNotExist() - waitForTag("photo.image:256") - ui.onNodeWithText("Preview photo").performClick() + // Seeded, camera and gallery photos are all AVAILABLE, so scope thumbnail and Preview to the newly captured camera row. + val cameraRow = "record.photo:${savedFile.getString("id")}" + ui.onNodeWithTag("record.photos.list").performScrollToNode(hasTestTag(cameraRow)) + val cameraThumbnail = hasTestTag("photo.image:256") and hasAnyAncestor(hasTestTag(cameraRow)) + ui.waitUntil(timeoutMillis = 60_000) { runCatching { ui.onNode(cameraThumbnail).fetchSemanticsNode() }.isSuccess } + ui.onNode(hasText("Preview photo") and hasAnyAncestor(hasTestTag(cameraRow))).performClick() waitFor("Photo preview") waitForTag("photo.image:1600") ui.onNodeWithText("Close preview").performClick() @@ -561,7 +575,7 @@ class ScannerEndToEndTest { listOf(savedFile, gallerySaved).forEach { file -> val row = "record.photo:${file.getString("id")}" ui.onNodeWithTag("record.photos.list").performScrollToNode(hasTestTag(row)) - ui.onNode(hasText("Awaiting review") and hasAnyAncestor(hasTestTag(row))).assertIsDisplayed() + ui.onNode(hasText(expectedBadge) and hasAnyAncestor(hasTestTag(row))).assertIsDisplayed() } stage("native-photo-gallery-parent-scoped") ui.onNodeWithText("Actions", substring = false).performClick() diff --git a/scanner-host/src/test/java/io/qrun/mobile/scanner/PhotoUploadViewModelTest.kt b/scanner-host/src/test/java/io/qrun/mobile/scanner/PhotoUploadViewModelTest.kt index 2c4612e..28f7798 100644 --- a/scanner-host/src/test/java/io/qrun/mobile/scanner/PhotoUploadViewModelTest.kt +++ b/scanner-host/src/test/java/io/qrun/mobile/scanner/PhotoUploadViewModelTest.kt @@ -60,6 +60,54 @@ class PhotoUploadViewModelTest { } } + /******************************************************************************* + ** A host that does not opt in (generic servers) commits QUARANTINED: the saved photo + ** is reported as awaiting server review, never as available. + *******************************************************************************/ + @Test + fun quarantinedReceiptOnOtherHostsReportsAwaitingReview() = runTest() + { + val dispatcher = StandardTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + try { + val transport = FakeTransport().apply { lifecycle = "QUARANTINED" } + val model = PhotoUploadViewModel(transport, "session", target, files.newFolder(), dispatcher) { true } + advanceUntilIdle() + model.prepareCamera().writeBytes(original) + model.cameraResult(true) + advanceUntilIdle() + assertTrue(model.saved) + assertEquals("Saved photo. Awaiting server review.", model.message) + } finally { + Dispatchers.resetMain() + } + } + + /******************************************************************************* + ** An Echo host that admits approved-device captures commits AVAILABLE: the same unchanged + ** receipt flow reports a plain saved photo, with the exact original still verified. + *******************************************************************************/ + @Test + fun availableReceiptOnEchoHostReportsSavedWithoutReviewWait() = runTest() + { + val dispatcher = StandardTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + try { + val transport = FakeTransport().apply { lifecycle = "AVAILABLE" } + val model = PhotoUploadViewModel(transport, "session", target, files.newFolder(), dispatcher) { true } + advanceUntilIdle() + model.prepareCamera().writeBytes(original) + model.cameraResult(true) + advanceUntilIdle() + assertTrue(model.saved) + assertEquals("Saved photo.", model.message) + assertArrayEquals(original, transport.uploaded) + assertEquals("CAMERA", model.intent!!.origin) + } finally { + Dispatchers.resetMain() + } + } + /******************************************************************************* ** Clearing the process cancels in-flight transport before removing only the app-owned temporary original. *******************************************************************************/ @@ -215,6 +263,7 @@ class PhotoUploadViewModelTest { var finalizes = 0 var cancels = 0 var statusCalls = 0 + var lifecycle = "QUARANTINED" var loseCreate = false var loseFinalize = false var chunkGate: CompletableDeferred? = null @@ -282,7 +331,7 @@ class PhotoUploadViewModelTest { "items", JSONArray().put( JSONObject() .put("itemId", intent.itemId).put("position", 0).put("fileId", "file").put("versionId", 2).put("attachmentId", 3) - .put("sha256", intent.sha256).put("sizeBytes", intent.size).put("lifecycleAtCommit", "QUARANTINED") + .put("sha256", intent.sha256).put("sizeBytes", intent.size).put("lifecycleAtCommit", lifecycle) ) ) else JSONObject.NULL return JSONObject().put("uploadId", id).put("state", state).put("receipt", receipt) -- 2.54.0