Compare commits

...
Author SHA1 Message Date
james.maes 1f5fd2ef95 Merge pull request 'Add semantic Echo workflows and scanner device UX' (#10) from james.maes/echo-semantic-choices-review into feature/mobile-scanner-host
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 3s
QRun-IO/ci-policy: Shared Android CI / unit-test (pull_request) Successful in 3m46s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / android-test (pull_request) Successful in 9m51s
QRun-IO/ci-policy: Shared Android CI / secrets (pull_request) Successful in 18s
QRun-IO/ci-policy: Shared Android CI / package (pull_request) Successful in 11s
QRun-IO/ci-policy: Shared Android CI / gate (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / android-0 (pull_request) Successful in 14m21s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 3s
Reviewed-on: #10
2026-11-03 23:06:12 +00:00
james.maesandClaude Sonnet 5.5 2c7394a62f fix(android): clear scanner feedback when starting a new run
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: Shared Android CI / unit-test (pull_request) Successful in 4m0s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / android-test (pull_request) Successful in 10m0s
QRun-IO/ci-policy: Shared Android CI / secrets (pull_request) Successful in 18s
QRun-IO/ci-policy: Shared Android CI / package (pull_request) Successful in 12s
QRun-IO/ci-policy: Shared Android CI / gate (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / android-0 (pull_request) Successful in 14m44s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
Reset per-process widget state with the rest of the process view model.
A regression proves that step transitions retain feedback while reset drops it.

Verified 57 mobileapp JVM tests, scanner lint, debug assembly and source standards.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 16:46:33 -06:00
james.maesandClaude Sonnet 5.5 9cf789c5b1 docs: align scanner setup with current forge and release policy
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: Shared Android CI / unit-test (pull_request) Successful in 4m29s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / android-test (pull_request) Successful in 11m1s
QRun-IO/ci-policy: Shared Android CI / secrets (pull_request) Successful in 19s
QRun-IO/ci-policy: Shared Android CI / package (pull_request) Successful in 12s
QRun-IO/ci-policy: Shared Android CI / gate (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / android-0 (pull_request) Successful in 16m16s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 5s
Correct Java and QQQ requirements, distinguish Gitea checks from legacy
workflows, and retain the owner's deferred signing and distribution decision.
Documentation only; runtime code and installed APK are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 16:20:39 -06:00
james.maesandClaude Sonnet 5.5 4d11a9a626 docs: record verified scanner and server completion gates
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / unit-test (pull_request) Successful in 4m29s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / android-test (pull_request) Successful in 10m40s
QRun-IO/ci-policy: Shared Android CI / secrets (pull_request) Successful in 18s
QRun-IO/ci-policy: Shared Android CI / package (pull_request) Successful in 12s
QRun-IO/ci-policy: Shared Android CI / gate (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / android-0 (pull_request) Successful in 15m54s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
Record the installed APK, passed primary Android CI and RC.21 consumer
verification. Keep owner publication, physical acceptance, native provider
qualification and deferred signing distinct from completed source work.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:58:35 -06:00
james.maesandClaude Sonnet 5.5 f72b90d08c feat(scanner): complete device UX and Authentik acceptance harness
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / android-test (pull_request) Successful in 9m50s
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / secrets (pull_request) Successful in 19s
QRun-IO/ci-policy: Shared Android CI / package (pull_request) Successful in 13s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / gate (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / android-0 (pull_request) Successful in 15m3s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
QRun-IO/ci-policy: Shared Android CI / unit-test (pull_request) Successful in 4m25s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
Add scan value and symbology feedback, optional status wallpaper and managed server/orientation configuration, proven AIM profile readback, and administrator-controlled viewing UI. Extend native Echo acceptance for per-scan copies and add an opt-in real-provider Authentik harness isolated to the disposable test package.

Verified: 222 JVM tests, lint with zero errors/warnings, debug/E2E/test APK builds, 23 Node tests and source standards. Updated TC22 Echo acceptance passes ten stages with simulated input/printing. Actual Authentik desktop acceptance passes; native Authentik and physical trigger/print validation remain pending. Normal TC22 updated with the matching signer and data preserved.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:07:44 -06:00
e255b7341d feat(scanner): send immutable raw bytes to compatible semantic servers
Capture exact SDK bytes before callback return and submit them through the existing process API only after same-server mobile-scanners 1.5 discovery. Preserve legacy server payloads and pending event retries; redact capture string representations. Verify 185 JVM tests, zero lint errors or warnings, debug/E2E builds and QA test compilation. Hardware AIM extraction and native delivery qualification remain open; the operator installation is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 10:56:59 -06:00
james.maesandClaude Sonnet 5.5 ffc5b493fa docs: record verified local Echo setup and remaining release gates
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 07:53:42 -06:00
james.maesandClaude Sonnet 5.5 fb3cc5d2f8 fix(android): send native request origins for cookie sessions
Derive Origin from each QQQ request URL after caller headers so strict same-origin servers accept session heartbeats and process actions. Verify 174 JVM tests, lint, APK build, and actual TC22 sign-in, Echo setup and cancellation without clearing data.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 07:10:00 -06:00
james.maesandClaude Sonnet 5.5 47a923a712 test(android): verify Echo document selection on native forms
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:21:44 -06:00
forge-recovery a2f0080755 Merge pull request 'Merge retained GitHub work into the Gitea primary branch' (#9) from migration/github-cutoff-reconcile-20261101 into feature/echo-scanner-presentation 2026-11-01 23:05:30 +00:00
james.maesandClaude Sonnet 5.5 c55d3002b0 test(ci): run no-device Echo guards
Run the eight fixture, device-identity and redaction guards in the existing verify lane without introducing device operations or additional dependencies.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 16:12:32 -06:00
james.maesandClaude 3bec46d35a feat(android): render semantic process choices for Echo
Load named possible values through the published process API, honor dependent fields, discard stale responses and keep errors retryable. Add isolated native Echo coverage for setup, two accepted scans, recreation and Apps cancellation.

Validation: 169 JVM tests, zero-warning lint, 8 guard tests, source standards and real TC22 simulated Echo E2E pass; all 86 requests match the published semantic API. Physical printing remains pending.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-01 15:49:03 -06:00
james.maesandGPT-6 Codex de3f6f6b03 docs(echo): record tested presentation and integration gates
Record the167-case client checkpoint and actual installed About verification while keeping owner-adapter/native/physical gates explicit.

Co-Authored-By: GPT-6 Codex <noreply@openai.com>
2026-11-01 12:41:23 -06:00
james.maesandCodex cd8c9f2a8c docs(scanner): document semantic Echo presentation hints
Describe the optional1.4 hints and unchanged receipt rules; correct the trusted EMDK path and avoid stale fixed demo-version claims.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-01 12:34:10 -06:00
james.maesandCodex 6293886f59 feat(scanner): honor semantic acknowledgment and manual-entry hints
Read optional bounded acknowledgmentLabel and Boolean manualEntryEnabled from SCANNER metadata. Existing servers retain Saved scan and manual entry; Echo can show Print queued and hide unsupported manual input. Hardware eligibility, receipt identity and lifecycle are unchanged.

Validation:167 JVM tests, strict Kotlin compilation, lint0errors/0warnings and debug assembly pass. Native Echo acceptance remains with the final real-server integration cohort.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-01 12:32:44 -06:00
james.maesandCodex 625a275de3 fix(quality): apply client standards review and add Query About
Preserve ownership notices, replace wildcard imports, document source contracts, enforce source checks and strict Kotlin warnings, and apply the repository native formatter policy. Correct typed widget state and callback registration with regression coverage.

Add bundled Query artwork, public Dogfooding and Query links, version and license access under Settings About. Validation:163 JVM cases,46 TC22 sample native cases, QA compilation, APK builds and lint0errors/0warnings. Full formatter CI and actual external-provider Android acceptance remain explicit.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-01 11:38:09 -06:00
james.maesandCodex 1127e4e4fa test: isolate location permissions in scanner UI acceptance
Include diagnostics UI acceptance in the hosted suite. Pregrant location only for disposable E2E scenarios and preserve session-monitor denied telemetry with explicit revocation and a handled-request marker. Production and QA permissions are untouched; the actual Android prompt remains separate acceptance.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-11-01 10:41:04 -06:00
james.maesandCodex 3fee63f795 feat: improve diagnostics and default foreground location on
Replace the raw log dialog with readable full-screen status/activity, filtering and copy/share controls. Persist explicit location choices and request Android permission once for default-on foreground reporting. Verified 159 JVM tests, zero-warning lint, APK builds and the focused TC22 diagnostics interaction test; actual GPS fix and real-provider acceptance remain pending.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-11-01 10:29:41 -06:00
james.maesandCodex 1c766bf062 feat: validate native OAuth sessions and support company sign-out
Require negotiated original-nonce validation for native OAuth and add optional state-bound AppAuth provider logout. Preserve TABLE login and local QQQ sign-out. Verified 151 JVM tests, lint, APK builds and 28 disposable TC22 native tests; real-provider and paired-server acceptance remain pending.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-11-01 09:45:52 -06:00
james.maesandCodex 8d8e500bf2 docs: reconcile current scanner release acceptance
Record passing portrait-default CI and physical Code128 evidence separately from pending provider, QR, camera and device acceptance. Preserve historical checkpoints and explicit dependency adoption limits.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-01 06:39:59 -06:00
james.maesandCodex 2e0fd962a7 docs: publish a verified leading-zero QR test fixture
Co-Authored-By: Codex <noreply@openai.com>
2026-11-01 06:27:17 -06:00
james.maesandOpenAI Codex 7de2b5088f fix: default the scanner to portrait orientation
Use Portrait when no valid orientation preference is saved; preserve all
explicit user choices, including System default. Cover missing, invalid,
explicit and platform orientation behavior in four focused tests.

Focused tests, debug lint (zero errors/warnings) and debug assembly pass.
A matching-signer in-place TC22 update preserved login/enrollment and
Android reports the normal activity requested portrait. Record exact
physical Code128 evidence and fully green prior native CI separately.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-01 05:44:14 -06:00
james.maesandOpenAI Codex 229fce30a1 test: wait for completed logout before fresh sign-in
Wait for the final signed-out message and enabled login action before
clicking once. Retain fresh-login and new-session assertions in the
real-server session scenario and opt-in device-screen acceptance.

The previous PR native run passed all seven scenarios; its duplicate
push run timed out after clicking the logout label. This test-readiness
correction is independently reviewed and both native test packages
compile. New hosted execution is required; no product fix is claimed.

Record the independent release-automation review, external configuration
gaps and current operator upgrade/projection acceptance boundaries.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-01 04:58:00 -06:00
james.maesandOpenAI GPT-6.1 5ba87f49d2 test: synchronize rotation and add opt-in screen acceptance
Wait for applied landscape configuration before explicit recreation and
assert the barcode draft after both transitions. Retain focused unlocked
input requirements while allowing brief physical-test focus changes.

Add a manual-only QA device-screen acceptance path with three actual OS
consent rounds, real Home-frame comparison, Stop, lease expiry and forced
logout. Protect existing app IDs/ports and compile QA tests in CI.
Use non-streaming physical installs and actionable timeout diagnostics.

QA and E2E packages compile, runner syntax/actionlint and opt-in/protected
port negative controls pass. Actual screen consent and synchronized
native scenario execution remain pending; no production code changed.

Co-Authored-By: OpenAI GPT-6.1 (Codex) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-01 04:24:15 -06:00
james.maesandOpenAI GPT-6.1 e04fdb2d21 fix: retain fresh screen frames and gate sharing startup
Drain ImageReader continuously while retaining only one latest image,
release replaced buffers, snapshot acquisition time before encoding,
and keep Share disabled until service binding finishes. Pin hosted
acceptance to the verified additive server1.2 source1041693.

Pass137 JVM tests, lint/all package builds, fresh APK inspection and
TC22 ordinary-UID forged-input rejection. Independent follow-up review
found no concrete issue. Paired server consumer/provenance and all15
browser cases pass. Actual OS consent/device capture remains unverified.

Co-Authored-By: OpenAI GPT-6.1 (Codex) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 21:51:12 -05:00
james.maesandOpenAI GPT-6.1 fbf9a30a51 feat: add consent-based device screen viewing
Add an optional nonexported MediaProjection foreground service, one-time
operator consent and session/lease/mode-bound frame upload. Preserve
legacy app-content defaults and stop capture on bounded lifetime or
host/session loss. Clarify physical scan polling deadline failures.

Initial gates pass135 JVM tests, lint and all package builds. Paired
server39 tests and six real-HTTP cases pass with historical API baselines
preserved. Changed-client static review found no reportable findings.
Actual consent/device acceptance and frame/UI refinements remain open;
this is a source checkpoint, not a production release.

Co-Authored-By: OpenAI GPT-6.1 (Codex) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 21:38:12 -05:00
james.maesandOpenAI GPT-6.1 6f25562919 test: diagnose hosted Back window focus failures
Report bounded own-window flags, focused-owner category, and network
dialog visibility when the existing five-second focus guard fails.
Keep real Back injection and its focus requirement unchanged.

Sample instrumentation compilation passes. CircleCI's three failures
remain unresolved pending a fresh diagnostic run. Record the completed
data-preserving local API1.9 upgrade and unverified physical scan attempt.

Co-Authored-By: OpenAI GPT-6.1 (Codex) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 21:23:34 -05:00
james.maesandOpenAI GPT-6.1 6845abc528 test: select explicit scan-probe package targets
Map accepted control values to compile-time QA/E2E package literals
before constructing broadcasts. Unknown targets still return before any
send, and marker validation/actions/permissions/counts remain unchanged.
This makes the existing allowlist boundary explicit to static analysis
without suppressing CodeQL or forwarding the caller's target string.

Probe build/lint and the distinct-signer ordinary-UID device acceptance
pass: eight forged broadcasts create no extra process steps or records,
and the manual positive control saves. Production APKs are unchanged.
Static triage and fresh hosted analysis remain separate evidence.

Co-Authored-By: OpenAI GPT-6.1 (Codex) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 20:50:24 -05:00
james.maesandOpenAI GPT-6.1 d316567fdc feat: secure Zebra scanning and unify mobile Back routing
Use the optional device-provided EMDK service for normal scanner input,
with exact compile-only SDK provenance and no shipped vendor classes.
Keep synthetic scan broadcasts confined to the disposable E2E variant.
Pause input behind overlays and lost focus, and share live drawer-first
Back routing across every process/application handler and toolbar arrow.

Replace inherited Auth0/Coil prereleases with reviewed stable versions.
Invalidate standard generated notices on dependency graph changes and
retain strict runtime/license checks. Pin hosted acceptance to the
source-qualified API 1.9 core/server tuple with runtime provenance.

Validation: 127 JVM tests, 34 TC22 shared native cases, eight-overlay
native regression, and ordinary-UID/distinct-signer injection rejection
pass. Eight forged broadcasts cause no process/record mutation; a real
manual positive control saves through the API. All package/lint gates,
four-APK notice/DEX/native/manifest inspection, focused independent
source review, workflow syntax and staged secret checks pass.

Actual hardware triggers, configured provider, optional files deployment
and final signing/distribution remain separate documented release gates.
No normal operator installation or data was changed by these tests.

Co-Authored-By: OpenAI GPT-6.1 (Codex) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 20:18:41 -05:00
james.maesandOpenAI GPT-6.1 df532ff514 fix: isolate operator login and logout lifecycles
Bind password and legacy Auth0 responses to their initiating origin,
metadata and generation. Defer cookie adoption, consume exchanges once,
and revoke known abandoned sessions at their original server. Make
local logout independent of provider callbacks and idempotent across
overlapping calls, delayed storage and screen remounts.

Exclude sample session storage from backup and transfer, and restrict
sample cleartext environments to debug builds. Document the remaining
trusted-scanner input finding and the real-provider acceptance boundary.

Validation:109 JVM tests and scanner lint pass;32 selected TC22 native
tests pass, including ten new authentication lifecycle regressions.
Packaged unsigned sample release denies cleartext and backup, with all
ten cloud/device-transfer exclusions. Operator installation is unchanged.

Co-Authored-By: OpenAI GPT-6.1 (Codex) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 17:37:39 -05:00
james.maesandOpenAI GPT-6.1 7e0cbee4fe build: align the verified Gradle wrapper with 8.7
Replace the older official bootstrap with the checksum-verified8.7 JAR,
keeping the configured distribution version and hash unchanged. Record
upstream provenance and the distinction from a complete binary audit.

Co-Authored-By: OpenAI GPT-6.1 (Codex) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 17:37:16 -05:00
james.maesandGPT-6.1 Codex d1de0bfc70 fix: let operators leave scans with unconfirmed results
Replace the permanent settled-scan navigation block with an explicit warning.
Keep the original retry identity through failed cancellation, and provide a
separate Leave anyway choice only after cancellation fails. In-flight work,
pending photos and complete receipt checks remain protected.

Add JVM and native regressions for lost or malformed acknowledgments, Stay,
cancel retries and explicit local exit without replaying a scan.

Co-Authored-By: GPT-6.1 Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 15:51:37 -05:00
james.maesandGPT-6.1 Codex 459cc1faec fix: handle Back while the navigation drawer opens
Consume Back when the drawer targets Open so a quick hardware press does not
exit the signed-out activity. Settle the native navigation test before Back
and retain the successful TC22 rerun: all 20 selected cases passed, including
seven synthetic AppAuth scenarios. Actual provider acceptance stays pending.

Co-Authored-By: GPT-6.1 Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 15:33:59 -05:00
james.maesandGPT-6.1 Codex b511e8ca66 feat: add semantic browser login and a single navigation drawer
Use one hamburger drawer for apps, Settings and Diagnostics while preserving
guarded process Back, account access and approval restrictions.

Negotiate native OAuth2 login through the explicit UI API 1.9 capability,
server-advertised issuer/callback/prompt policy and AppAuth PKCE. Exchange
codes through QQQ with deferred cookie adoption and bounded cleanup of known
abandoned sessions. Keep TABLE_BASED and shared Auth0 behavior intact.

Add protocol and native regressions, retain bundled AppAuth attribution,
and pin the companion server heartbeat revocation correction in CI.

Co-Authored-By: GPT-6.1 Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 15:23:40 -05:00
james.maesandGPT-6.1 Codex d5b5580d60 feat: add mobile sessions, record photos and guarded navigation
Add semantic session monitoring and leased app-screen viewing, optional record
photo capture, device orientation, native navigation guards and TC22 acceptance.
Bundle verified dependency and native notices with fail-closed packaging checks.

Co-Authored-By: GPT-6.1 Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 07:21:38 -05:00
james.maesandOpenAI GPT-6 6574ec4991 fix: enforce semantic UI API contracts in scanner host
Discover compatible API versions, client channels and mobile extensions before login. Follow server process completion, metadata updates, form rules and cancellation, with isolated native OpenAPI conformance coverage.

Co-Authored-By: OpenAI GPT-6 (Codex) <noreply@openai.com>
2026-10-30 18:30:01 -05:00
james.maesandOpenAI GPT-6 13391d210e test: cover scanner workflow against the real QQQ server
Co-Authored-By: OpenAI GPT-6 (Codex) <noreply@openai.com>
2026-10-30 16:42:04 -05:00
james.maesandOpenAI GPT-6 e53e503871 fix: avoid false offline blocking in scanner host
Co-Authored-By: OpenAI GPT-6 (Codex) <noreply@openai.com>
2026-10-30 09:07:03 -05:00
james.maes b7c58f891a docs: record emulator acceptance and local restart steps 2026-10-29 15:43:50 -05:00
james.maes a5621c586a feat: add customer branding and live connection diagnostics 2026-10-29 15:18:36 -05:00
james.maes dfdbb74d21 feat: add native scanner host and Android verification workflows 2026-10-29 14:16:19 -05:00
james.maesandCodex 63fca899a9 ci: adopt managed orb 0.8 patch track (#7)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 16:04:29 -05:00
james.maesandCodex 88da04a124 ci: adopt shared orb 0.7.17 (#6)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 14:26:01 -05:00
james.maesandCodex 0b88114c81 ci: adopt central orb 0.7.12 and stable gates (#5)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 03:36:23 -05:00
james.maesandCodex cd034e0aab ci: adopt corrected shared package publisher from orb 0.7.11 (#4)
* ci: adopt shared GitHub package publisher from orb 0.7.10

Co-Authored-By: Codex <noreply@openai.com>

* ci: use corrected Android continuation from orb 0.7.11

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
2026-10-20 22:17:34 -05:00
james.maesandCodex 674246bbe8 ci: adopt shared Android publication credentials (#3)
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 21:21:49 -05:00
james.maesandCodex 5c9a829f7f fix: publish Android modules to current QRun registry (#2)
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 20:46:36 -05:00
2517 changed files with 139605 additions and 60936 deletions

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.7.4
qqq-orb: kingsrook/qqq-orb@0.8
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: 0.7.4
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 3
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[*.{cmd,bat}]
end_of_line = crlf
+5
View File
@@ -0,0 +1,5 @@
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.jar binary
*.png binary
+1
View File
@@ -0,0 +1 @@
* @KofTwentyTwo
+25
View File
@@ -0,0 +1,25 @@
---
name: Bug report
about: Report an Android client or scanner problem
labels: bug
---
## What happened?
Describe expected and actual behavior.
## Reproduction
1.
2.
## Environment
- APK version/build:
- QQQ server/qbit version:
- Android version and device model:
- DataWedge version/profile settings, if applicable:
## Evidence
Use synthetic barcodes. Remove credentials, device tokens, session cookies, and personal/business data from screenshots and logs. Report security vulnerabilities privately using SECURITY.md.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Questions and discussion
url: https://github.com/orgs/QRun-IO/discussions
about: Ask the QQQ community about application development.
- name: Security reporting
url: https://github.com/QRun-IO/qqq-android/blob/main/SECURITY.md
about: Report vulnerabilities privately rather than opening a public issue.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Describe a mobile workflow or Android capability
labels: enhancement
---
## User workflow
Who needs this, and what are they trying to do?
## Proposed behavior
Describe the screens, scanner interaction, or QQQ process behavior.
## Scope and acceptance
List target devices and concrete examples of success. Use synthetic data.
+14
View File
@@ -0,0 +1,14 @@
## Summary
Describe the user-visible change and link related issues.
## Validation
- [ ] Shared-library and scanner JVM tests passed
- [ ] Scanner lint and debug APK build passed
- [ ] Native emulator tests or manual UI verification completed when relevant
- [ ] TC22/DataWedge behavior verified when relevant, or the remaining hardware check is stated
## Compatibility and rollout
Describe API/metadata changes, credential or enrollment changes, and any required server upgrade. Include screenshots for UI changes. Never include passwords, session cookies, device tokens, or real barcode data.
+14
View File
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: gradle
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 3
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns: ['*']
+138
View File
@@ -0,0 +1,138 @@
name: Release scanner APK
on:
push:
tags: ['scanner-v*']
permissions:
contents: read
concurrency:
group: scanner-release
cancel-in-progress: false
jobs:
verify:
uses: ./.github/workflows/scanner.yml
package:
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 30
environment: scanner-release
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
fetch-depth: 0
- name: Require main ancestry and a strictly newer Android version
id: release-version
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
SCANNER_VERSION_CODE: ${{ vars.SCANNER_VERSION_CODE }}
shell: bash
run: |
set -euo pipefail
git merge-base --is-ancestor "$GITHUB_SHA" refs/remotes/origin/main
[[ "$SCANNER_VERSION_CODE" =~ ^[1-9][0-9]{0,9}$ ]] || exit 1
(( SCANNER_VERSION_CODE <= 2100000000 )) || exit 1
gh api --paginate "repos/$GH_REPO/releases?per_page=100" \
--jq '.[] | select(.draft == false) | select(.tag_name | startswith("scanner-v")) | .tag_name' > "$RUNNER_TEMP/scanner-prior-tags.txt"
while IFS= read -r tag; do
[[ "$tag" =~ ^scanner-v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]] || exit 1
prior_directory=$(mktemp -d "$RUNNER_TEMP/scanner-prior.XXXXXX")
gh release download "$tag" --pattern BUILD.txt --dir "$prior_directory"
previous=$(sed -n 's/^Version code: //p' "$prior_directory/BUILD.txt")
[[ "$previous" =~ ^[1-9][0-9]{0,9}$ ]] || exit 1
(( SCANNER_VERSION_CODE > previous )) || { echo 'Version code must exceed every published scanner release'; exit 1; }
done < "$RUNNER_TEMP/scanner-prior-tags.txt"
printf 'code=%s\n' "$SCANNER_VERSION_CODE" >> "$GITHUB_OUTPUT"
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: temurin
java-version: '21'
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4
- name: Install Android SDK packages
run: |
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-35" "build-tools;34.0.0" "build-tools;35.0.0" "platform-tools"
- name: Build and verify signed release
env:
SCANNER_KEYSTORE_BASE64: ${{ secrets.SCANNER_KEYSTORE_BASE64 }}
SCANNER_STORE_PASSWORD: ${{ secrets.SCANNER_STORE_PASSWORD }}
SCANNER_KEY_ALIAS: ${{ secrets.SCANNER_KEY_ALIAS }}
SCANNER_KEY_PASSWORD: ${{ secrets.SCANNER_KEY_PASSWORD }}
SCANNER_CERTIFICATE_SHA256: ${{ vars.SCANNER_CERTIFICATE_SHA256 }}
SCANNER_VERSION_CODE: ${{ steps.release-version.outputs.code }}
shell: bash
run: |
set -euo pipefail
umask 077
version="${GITHUB_REF_NAME#scanner-v}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]] || exit 1
[[ "$SCANNER_VERSION_CODE" =~ ^[1-9][0-9]{0,9}$ ]] || exit 1
(( SCANNER_VERSION_CODE <= 2100000000 )) || exit 1
[[ "$SCANNER_CERTIFICATE_SHA256" =~ ^[A-Fa-f0-9]{64}$ ]] || exit 1
for name in SCANNER_KEYSTORE_BASE64 SCANNER_STORE_PASSWORD SCANNER_KEY_ALIAS SCANNER_KEY_PASSWORD; do
[[ -n "${!name}" ]] || { echo "Missing release signing configuration: $name"; exit 1; }
done
export SCANNER_KEYSTORE="$RUNNER_TEMP/scanner-release.jks"
trap 'rm -f -- "$SCANNER_KEYSTORE"' EXIT
printf '%s' "$SCANNER_KEYSTORE_BASE64" | base64 --decode > "$SCANNER_KEYSTORE"
unset SCANNER_KEYSTORE_BASE64
./gradlew --no-daemon --console=plain :scanner-host:lintRelease :scanner-host:assembleRelease \
"-PscannerVersionName=$version" "-PscannerVersionCode=$SCANNER_VERSION_CODE"
mkdir -p release
cp scanner-host/build/outputs/apk/release/scanner-host-release.apk "release/qqq-mobile-$version.apk"
cp scanner-host/build/reports/third-party/release/{aboutlibraries.json,LICENSE,NOTICE} release/
"$ANDROID_HOME/build-tools/35.0.0/apksigner" verify --verbose --print-certs "release/qqq-mobile-$version.apk" > release/signature.txt
certificate=$(sed -n 's/^Signer #1 certificate SHA-256 digest: //p' release/signature.txt)
[[ "${certificate,,}" == "${SCANNER_CERTIFICATE_SHA256,,}" ]] || { echo 'Unexpected signing certificate'; exit 1; }
"$ANDROID_HOME/build-tools/35.0.0/aapt" dump badging "release/qqq-mobile-$version.apk" > "$RUNNER_TEMP/scanner-manifest.txt"
grep -F "package: name='io.qrun.mobile.scanner' versionCode='$SCANNER_VERSION_CODE' versionName='$version'" "$RUNNER_TEMP/scanner-manifest.txt"
if grep -q 'application-debuggable' "$RUNNER_TEMP/scanner-manifest.txt"; then
echo 'Refusing a debuggable release'; exit 1
fi
printf 'Source: %s\nVersion: %s\nVersion code: %s\n' "$GITHUB_SHA" "$version" "$SCANNER_VERSION_CODE" > release/BUILD.txt
(cd release && sha256sum -- *.apk signature.txt BUILD.txt aboutlibraries.json LICENSE NOTICE > SHA256SUMS)
- name: Retain signed deliverable
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: scanner-release-${{ github.sha }}
path: release/
if-no-files-found: error
retention-days: 30
publish:
needs: package
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: scanner-release-${{ github.sha }}
path: release
- name: Publish immutable-tag APK assets
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
shell: bash
run: |
set -euo pipefail
(cd release && sha256sum --check SHA256SUMS)
prerelease=()
version="${GITHUB_REF_NAME#scanner-v}"
tag_object=$(gh api "repos/$GH_REPO/git/ref/tags/$GITHUB_REF_NAME" --jq '.object.sha')
tag_type=$(gh api "repos/$GH_REPO/git/ref/tags/$GITHUB_REF_NAME" --jq '.object.type')
for ((attempt=0; attempt<5; attempt++)); do
if [[ "$tag_type" == commit ]]; then break; fi
[[ "$tag_type" == tag ]] || exit 1
tag_json=$(gh api "repos/$GH_REPO/git/tags/$tag_object")
tag_object=$(jq -r '.object.sha' <<< "$tag_json")
tag_type=$(jq -r '.object.type' <<< "$tag_json")
done
[[ "$tag_type" == commit && "$tag_object" == "$GITHUB_SHA" ]] || { echo 'Release tag no longer identifies the verified source'; exit 1; }
if [[ "$version" == *-* ]]; then prerelease=(--prerelease); fi
gh release create "$GITHUB_REF_NAME" --verify-tag --title "QQQ Mobile $version" \
--generate-notes "${prerelease[@]}" release/*
+163
View File
@@ -0,0 +1,163 @@
name: Scanner Android
on:
push:
branches: [main, 'feature/**', 'fix/**', 'release/**', 'hotfix/**']
pull_request:
workflow_call:
workflow_dispatch:
inputs:
emulator:
description: Run native UI and real-server scanner end-to-end tests
type: boolean
default: true
permissions:
contents: read
concurrency:
group: scanner-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Tests, lint, and debug APK
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: temurin
java-version: '21'
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4
with:
cache-read-only: ${{ github.event_name == 'pull_request' }}
- name: Verify source headers, whitespace, imports and JavaScript syntax
run: node scripts/ci/check-source-standards.mjs
- name: Run no-device Echo fixture guard tests
run: node --test scripts/ci/scanner-echo-guards.test.mjs
- name: Install required Android SDK packages
run: |
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-35" "build-tools;34.0.0" "build-tools;35.0.0" "platform-tools"
- name: Verify shared libraries and scanner host
run: >-
./gradlew --no-daemon --console=plain
:qqq:android:core:testDebugUnitTest
:qqq:android:mobileapp:testDebugUnitTest
:scanner-host:testDebugUnitTest
:scanner-host:lintDebug
:scanner-host:assembleDebug
- name: Compile opt-in physical QA acceptance
run: >-
./gradlew --no-daemon --console=plain -PscannerTestBuildType=qa
:scanner-host:assembleQa :scanner-host:assembleQaAndroidTest
- name: Hash debug APK
run: |
mkdir -p scanner-host/build/outputs/apk/debug/notices
cp scanner-host/build/reports/third-party/debug/{aboutlibraries.json,LICENSE,NOTICE} scanner-host/build/outputs/apk/debug/notices/
(cd scanner-host/build/outputs/apk/debug && sha256sum scanner-host-debug.apk notices/* > SHA256SUMS)
- name: Upload installable development APK
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: scanner-debug-${{ github.sha }}
path: scanner-host/build/outputs/apk/debug/
if-no-files-found: error
retention-days: 14
- name: Upload test and lint evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: scanner-checks-${{ github.sha }}
path: |
qqq/android/core/build/test-results/
qqq/android/mobileapp/build/test-results/
scanner-host/build/test-results/
scanner-host/build/reports/
retention-days: 14
emulator:
name: Native UI and real-server scanner E2E
if: github.event_name != 'workflow_dispatch' || inputs.emulator
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 40
env:
QQQ_CORE_SHA: ecf0f550a0357cec6387c2b54efca128a405a629
SCANNER_SERVER_SHA: 10416938724c9663d66f7848ff2b1a63fe635794
MAVEN_OPTS: -XX:ActiveProcessorCount=2 -Xmx768m
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: temurin
java-version: '21'
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4
- name: Check out the pinned QQQ core source
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
repository: QRun-IO/qqq
ref: ${{ env.QQQ_CORE_SHA }}
path: .e2e-core
persist-credentials: false
- name: Check out the pinned real QQQ demo
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
repository: QRun-IO/qbit-mobile-scanners
ref: ${{ env.SCANNER_SERVER_SHA }}
path: .e2e-server
persist-credentials: false
- name: Verify and build the isolated source-pinned server fixture
env:
JAVA_TOOL_OPTIONS: -XX:ActiveProcessorCount=2 -Xmx768m
run: |
test "$(git -C .e2e-core rev-parse HEAD)" = "$QQQ_CORE_SHA"
test "$(git -C .e2e-server rev-parse HEAD)" = "$SCANNER_SERVER_SHA"
fixture_m2=$(mktemp -d "$RUNNER_TEMP/scanner-fixture-m2.XXXXXX")
core_revision="4.1.0-ci.$QQQ_CORE_SHA"
(cd .e2e-core && ./mvnw --batch-mode --no-transfer-progress \
-Dmaven.repo.local="$fixture_m2" -Drevision="$core_revision" \
-pl qqq-bom,qqq-middleware-api -am clean install)
(cd .e2e-server && ./mvnw --batch-mode --no-transfer-progress \
-Dmaven.repo.local="$fixture_m2" -Dqqq.version="$core_revision" clean install)
(cd .e2e-server && ./mvnw --batch-mode --no-transfer-progress -f demo/pom.xml \
-Dmaven.repo.local="$fixture_m2" -Dqqq.version="$core_revision" clean verify \
dependency:build-classpath -DincludeScope=runtime -Dmdep.outputFile=target/e2e-classpath.txt \
dependency:tree -Dscope=runtime -DoutputFile=target/fixture-dependencies.txt \
help:effective-pom -Doutput=target/fixture-effective-pom.xml)
node scripts/ci/verify-fixture-provenance.mjs .e2e-server "$fixture_m2" "$QQQ_CORE_SHA" "$SCANNER_SERVER_SHA"
- name: Enable hosted-runner hardware acceleration
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run emulator checks
uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
with:
api-level: 35
target: google_apis
arch: x86_64
disable-animations: true
script: |
./gradlew --no-daemon --console=plain :qqqsampleandroidmobileapp:connectedDebugAndroidTest :scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest
node scripts/ci/scanner-e2e.mjs .e2e-server
- name: Upload emulator evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: scanner-emulator-${{ github.sha }}
include-hidden-files: true
path: |
qqqsampleandroidmobileapp/build/reports/androidTests/
qqqsampleandroidmobileapp/build/outputs/androidTest-results/
scanner-host/build/native-e2e/
.e2e-server/demo/target/fixture-provenance.json
.e2e-server/demo/target/fixture-dependencies.txt
.e2e-server/demo/target/fixture-effective-pom.xml
.e2e-core/**/target/surefire-reports/TEST-*.xml
.e2e-server/**/target/surefire-reports/TEST-*.xml
retention-days: 14
+4
View File
@@ -13,3 +13,7 @@
.externalNativeBuild
.cxx
local.properties
/.e2e-server/
/.e2e-core/
*.jks
*.keystore
+15
View File
@@ -0,0 +1,15 @@
<!-- Copyright (c) 2026 Kingsrook, LLC. SPDX-License-Identifier: Apache-2.0 -->
<code_scheme name="QQQ Android repository" version="173">
<option name="RIGHT_MARGIN" value="1000000" />
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="INDENT_SIZE" value="3" />
<option name="CONTINUATION_INDENT_SIZE" value="3" />
<option name="TAB_SIZE" value="3" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
+5
View File
@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectCodeStyleConfiguration">
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</component>
</project>
+1 -1
View File
@@ -1,6 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="QQQ - Low-code Application Framework for Engineers.&#10;Copyright (C) 2004-&amp;#36;today.year. Kingsrook, LLC&#10;651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States&#10;contact@kingsrook.com&#10;https://github.com/Kingsrook/&#10;&#10;This program is free software: you can redistribute it and/or modify&#10;it under the terms of the GNU Affero General Public License as&#10;published by the Free Software Foundation, either version 3 of the&#10;License, or (at your option) any later version.&#10;&#10;This program is distributed in the hope that it will be useful,&#10;but WITHOUT ANY WARRANTY; without even the implied warranty of&#10;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10;GNU Affero General Public License for more details.&#10;&#10;You should have received a copy of the GNU Affero General Public License&#10;along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;.&#10;" />
<option name="notice" value="QQQ - Low-code Application Framework for Engineers.&#10;Copyright (C) 2004-&amp;#36;today.year. Kingsrook, LLC&#10;651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States&#10;contact@kingsrook.com&#10;https://github.com/Kingsrook/&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10; https://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License.&#10;" />
<option name="myName" value="Kingsrook" />
</copyright>
</component>
+1 -1
View File
@@ -1,3 +1,3 @@
profile: android
orb_version: 0.7.4
orb_version: '0.8'
default_branch: main
+27
View File
@@ -0,0 +1,27 @@
# Repository guidance
Read `README.md`, `docs/SESSION-STATE.md`, and `docs/TODO.md` for current scope. Preserve unrelated changes and follow the user's current authorization for commits, publication, and deployment.
## Build and tests
Use the included Gradle wrapper, Java21, and Android SDK35. `local.properties`, build outputs, SDK paths, and credentials are local-only.
```text
./gradlew :qqq:android:core:testDebugUnitTest :qqq:android:mobileapp:testDebugUnitTest :scanner-host:testDebugUnitTest :scanner-host:lintDebug :scanner-host:assembleDebug
```
Run `:qqqsampleandroidmobileapp:connectedDebugAndroidTest` on an emulator for changes to native authentication/navigation/process rendering. `scripts/ci/emulator-smoke.sh` deliberately refuses physical devices and clears only the disposable emulator's scanner application data. Physical TC22 tests remain necessary for EMDK trigger capture, lifecycle and scanner-ownership release. Use the separate production-path QA variant and separately signed ordinary-UID probe documented in `docs/ZEBRA-SCANNING.md`; E2E synthetic broadcasts do not qualify production scanner security or hardware behavior. Never replace the operator's application or persistently modify DataWedge profiles for these tests.
All mobile processes require end-to-end coverage. For scanner flow changes, build `:scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest` and run `node scripts/ci/scanner-e2e.mjs ../qbit-mobile-scanners` against an emulator after building the companion demo/classpath as documented in `docs/EMULATOR.md`. This drives the real host and real server in the separate `io.qrun.mobile.scanner.e2e` application with a temporary database; never clear the operator's normal scanner application to run it. Server record actions must also be covered through both single-record and selected-bulk UI paths in the companion qbit's browser suite.
## Design and safety
Reuse the existing `core` and `mobileapp` libraries and native QQQ metadata/process contracts. The scanner host adds enrollment and scanner widgets; do not introduce a competing screen engine. Preserve raw barcode strings, event identity across retries, server acknowledgments, and server-side device authorization.
All client/server operations must use the versioned semantic UI API or a registered, documented semantic extension. Discover version, required features, client channel and extension compatibility before credentials. Follow server process state and field metadata; never infer business actions from a process name or a generic field name. New required controls need native support and E2E coverage; unknown controls must stop submission explicitly. Keep the real-server OpenAPI request audit in CI.
Never log or place passwords, session cookies, or device tokens in process values, test fixtures, or documentation. Keep enrollment credentials and sessions isolated by server. Keep HTTP support limited to debug builds. Test resources under `mobile-contract` are sanitized real demo responses.
## Repository automation
The primary forge is Gitea (`git.kof22.com/QRun-IO`); GitHub is a shadow/archive, so make no ordinary GitHub writes. Protected `main` on Gitea has `.gitea/workflows/qrun-android-release.yaml` calling the shared `KofTwentyTwo/platform-ci` `android.yaml@qrun-release-v1` (not present in this feature working tree); pull-request checks come from central Gitea policy, not from that release workflow. CircleCI is managed by `kingsrook/qqq-orb` using `.qqq-ci.yml`; preserve that lane and its main-only library publication. The legacy `.github` `Scanner Android` workflow builds a debug APK artifact and never publishes libraries or production releases; it and the scanner release workflow are not a selected release path. Production signing/distribution is deferred: create no keys, tags or releases. Do not replace managed CI with a stale local template.
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## Unreleased
- Add the QQQ Mobile scanner host for metadata-defined native apps and processes.
- Add administrator-approved device enrollment and Zebra DataWedge single-barcode capture.
- Add table-backed login, current QQQ HttpOnly session handling, and server-side logout.
- Preserve pending scan identity for explicit retry and require a matching server acknowledgment.
- Add scanner APK/test/lint CI and an optional emulator workflow alongside managed library CI.
+3
View File
@@ -0,0 +1,3 @@
# Code of conduct
This project follows the [QRun-IO Contributor Covenant Code of Conduct](https://github.com/QRun-IO/.github/blob/main/CODE_OF_CONDUCT.md), including its reporting and enforcement process.
+31 -1
View File
@@ -1,4 +1,34 @@
# Contributing to QQQ Android
See the [Contributing Guidelines for QQQ](https://github.com/Kingsrook/qqq/blob/dev/CONTRIBUTING.md)
Follow the [QRun-IO contribution guidelines](https://github.com/QRun-IO/.github/blob/main/CONTRIBUTING.md) and this repository's existing Kotlin conventions. Use a `feature/`, `fix/`, `docs/`, or `chore/` branch from `main`, and open a pull request to `main`. Use Conventional Commits and include tests and documentation for changed behavior.
## Local verification
Install Java21 and Android SDK35, then use the included Gradle wrapper:
```text
./gradlew :qqq:android:core:testDebugUnitTest :qqq:android:mobileapp:testDebugUnitTest :scanner-host:testDebugUnitTest :scanner-host:lintDebug :scanner-host:assembleDebug
```
On Windows use `gradlew.bat`. Keep SDK locations in ignored `local.properties` or environment variables. Do not commit APKs, signing keys, passwords, session cookies, device tokens, or real barcode data.
For authentication/navigation/process UI changes, run the native emulator suite:
```text
./gradlew :qqqsampleandroidmobileapp:connectedDebugAndroidTest
bash scripts/ci/emulator-smoke.sh
```
The smoke script clears application data only after verifying the target is an emulator. See [emulator testing](docs/EMULATOR.md). DataWedge trigger behavior still needs a physical Zebra handheld.
## CI and delivery
Managed CircleCI remains the shared-library test and Maven publication lane. Its Android profile publishes libraries only from `main`, after its checks. Feature branches do not publish.
The GitHub **Scanner Android** workflow runs shared-library/scanner JVM tests, scanner lint, and debug APK assembly for pull requests and supported branches. It uploads the development APK with SHA256 and verification reports. **Run workflow** optionally adds native emulator tests and a scanner launch screenshot. This workflow never publishes Maven artifacts, signs a production APK, or deploys to a device/store.
Production signing and distribution require a separately configured release procedure. Do not treat the development APK as a production release.
## Review and security
Request review from the repository code owner. Explain compatibility changes between Android metadata/process contracts and the companion qbit. Use synthetic fixtures and preserve raw barcode values and retry identities. See [SECURITY.md](SECURITY.md) for private reporting and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for community expectations.
+26
View File
@@ -7,9 +7,35 @@ Licensed under the Apache License, Version 2.0.
================================================================================
Material Design icons by Google
The 2,142 vector resources in qqq/android/mobileapp/src/main/res/drawable
use Material Design icon artwork from Google, licensed under Apache-2.0:
https://github.com/google/material-design-icons
Pinned upstream revision: d7a78a2ad434d5d38e1b8708c9dd9847cc3ada9b
QQQ Android integration modifications include Android resource names, dimensions,
and fill/tint presentation. Each resource's drawing path data matches its pinned
upstream source. The loading and unavailable-network illustrations use the
upstream hourglass_empty and wifi_off icons, respectively.
See docs/material-icons-provenance.json for the complete resource-to-source
mapping, SHA-256 hashes, and final styling. The upstream license is retained in
docs/third-party/material-design-icons-LICENSE.txt.
Project copyright statements cover project integration/modifications and do not
replace Google's original artwork attribution. Dependency and embedded-data
notices are included in the application's Open-source licenses settings and its
matching generated distribution report. This repository notice supplements that
report; it is not a complete dependency inventory by itself.
================================================================================
Trademarks:
QQQ, qBits, qRun, qStudio, and the QRunIO logo are trademarks of QRunIO, LLC.
Use of these trademarks must comply with QRunIO's Trademark Guidelines.
================================================================================
Query mascot artwork: QRun's existing Query puppy asset, sourced from marketing-website/assets/Puppy Mark/exports/PNG/qqq-puppy.png. Bundled for the About screen; the original artwork is unchanged.
+53 -96
View File
@@ -1,122 +1,79 @@
# qqq-android
# QQQ Android
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
Native Kotlin/Compose clients for QQQ. The new `scanner-host` application runs server-defined QQQ apps and processes on Android handhelds, initially Zebra TC22.
Android client library for QQQ applications.
## Modules
**For:** Developers building native Android apps that connect to QQQ backends
**Status:** Experimental
- `qqq/android/core`: metadata and process HTTP client (`QQQRepository` / `NetworkQQQRepository`).
- `qqq/android/mobileapp`: native app picker, authentication, process runner, and widget renderer.
- `scanner-host`: server configuration, device registration/approval gate, and Zebra EMDK barcode capture.
- `qqqsampleandroidmobileapp`: historical example application.
## Why This Exists
The client supports metadata and process execution. It does not implement the record CRUD/fluent API previously described by this README.
QQQ applications expose REST APIs that any client can consume. For Android apps that need to interact with QQQ backends, this library provides typed clients and UI components that understand QQQ's metadata model.
The scanner host uses the versioned semantic UI API for app discovery, native process screens and actions, plus the registered `mobile-scanners` extension for device enrollment, branding, connectivity and session administration. It requires extension 1.1 or later within major 1 and checks compatibility before login. See [the client contract and supported components](docs/UI-API.md); full web-client feature coverage is not implied.
## Features
## First scanner application
- **API Client** - Typed client for QQQ REST endpoints
- **Metadata Loading** - Fetch table and field definitions
- **Record Operations** - Query, insert, update, delete records
- **Process Execution** - Run backend processes from Android
Use the companion `qbit-mobile-scanners` server/demo with its current QQQ source default (`4.1.0-RC.21`; see that repository's POM). A local server may run an older qualified core; the host checks the advertised UI API and extension versions before login. The demo uses QQQ TABLE_BASED username/password authentication. Credentials are exchanged through the native QQQ session endpoint; the password is not stored.
## Quick Start
1. Start the companion demo and install the debug APK.
2. Run `adb reverse tcp:8080 tcp:8080` for a USB-connected TC22.
3. Open QQQ Mobile, use `http://127.0.0.1:8080/`, and sign in.
4. An administrator approves the displayed device ID in QQQ.
5. Tap **Check approval**, select the server-provided mobile app, and open **Scan capture**.
6. Scan a barcode and wait for **Saved scan** before scanning again. Manual entry is available for development and non-Zebra devices.
### Prerequisites
The server defines the app, native process steps, and `SCANNER` widget; the APK does not hard-code a scan process name. Existing native widget types continue to work.
- Android Studio
- Android SDK 26+
- QQQ backend running
## Zebra hardware scanning
### Installation
The normal debug and release APKs use the device-provided Zebra EMDK service. They do not accept DataWedge scan broadcasts. No DataWedge profile setup is required. The scanner is acquired only while a scanner widget is visible and the activity is resumed; leaving or backgrounding releases it. EMDK temporarily takes scanner ownership from DataWedge and restores it on release. The app does not persistently modify DataWedge settings.
Add to `build.gradle`:
The same APK runs on other Android devices with explicit manual entry. When the required system library/service is missing or unavailable, the screen explains that hardware scanning is unavailable. [Scanner integration and SDK provenance](docs/ZEBRA-SCANNING.md) describes the lifecycle, compile-only SDK and verification gates. Synthetic broadcasts are accepted only by the separate disposable E2E variant.
```groovy
dependencies {
implementation 'io.qrun:qqq-android:0.1.0'
}
## Customer branding and diagnostics
The shared launcher name/icon remain QQQ Mobile. On connecting to a server, the host reads public `GET /qqq/v1/qbit/mobile-scanners/branding` (`displayName`, `primaryColor`, optional `logoUrl`). The name and Material theme apply to login, device approval, and native app screens. Names are limited to 80 printable characters and colors to `#RRGGBB`; malformed values fall back to QQQ defaults. Button foregrounds choose readable black/white contrast. Optional logos must use HTTPS, are fetched without session/device headers or cookies, and cannot prevent login if unavailable. Responses are size-limited, redirects are disabled, and branding is reset when changing servers. Reconnect to pick up server branding changes; no APK rebuild is needed.
The persistent footer shows **Checking**, **Connected**, or **Disconnected** with the last check time. It checks `GET /qqq/v1/qbit/mobile-scanners/status` every five seconds while foregrounded and on network changes; `Connected` means a valid `{ "status": "ok" }` response, not sign-in or device approval. Tap the footer, or use the Diagnostics entry in the menu, to open a full-screen diagnostics view with a server connection summary (reachability only), the recent activity newest first in local time, **All** and **Problems** filters, **Pause updates** and **Resume live updates**, **Copy**, **Share** and **Clear**, and **Back**. While it is open, process input stays blocked. The in-memory log holds the latest 100 fixed event codes with timestamps, HTTP status, and latency for branding, connection checks, native QQQ calls (including login/process requests), and device enrollment/status requests. A transport failure has no HTTP status; an HTTP response retains its status, including 401/403/500 errors. The log contains no credentials, scan values, request paths, queries, raw response text, or exception messages. Changing servers clears the displayed diagnostics. Monitoring never queues or retries a scan.
## Online-only behavior
QQQ administrators can inspect session activity, end sessions, and request live viewing of the QQQ app from a session record. Viewing displays an informational indicator on the handheld; the app has no operator pause or stop control. Password/login screens and background apps cannot be mirrored. Optional whole-device sharing still requires Android consent and honors Android's system stop controls. Foreground location reporting defaults on and can be disabled in Settings. It requires Android permission and enabled device location providers; scanning works without it. See [session behavior and limits](docs/UI-API.md#session-activity-and-screen-viewing).
Open **Settings → About QQQ Mobile** for the app version, Query mascot, [Dogfooding](https://www.qrun.io/dogfooding), [Query](https://www.qrun.io/query) and bundled open-source licenses. Public website links open in the external browser without adding session or device information.
There is no offline queue. One submitted scan is retained in memory until a complete receipt returns the matching `acknowledgedEventId`, `scanSaved: true`, and a nonblank `scanId`. If a response is uncertain, **Retry same request** preserves its event ID and original timestamp; the server deduplicates it. Further scans wait for confirmation or explicit abandonment. Leaving after an uncertain response warns that the scan may already be saved and attempts server cancellation; if cancellation fails, **Leave anyway** lets the operator exit explicitly. Active requests and pending photos remain protected. Sign-out/reset clears the pending request; app termination loses in-memory state. Repeated physical scans deliberately receive distinct event IDs.
Enrollment credentials are encrypted with Android Keystore and stored under `noBackupFilesDir`, scoped to the configured server. The app disables backup. Device headers accompany native QQQ requests, and the server enforces approval on every scan. Release builds reject HTTP; only the development build permits it for local testing. The host disables HTTP redirects to avoid forwarding credentials to another origin.
## Build
Requirements: Java 21, Android SDK platform35, build tools34/35, and the included Gradle8.7 wrapper. Configure `ANDROID_HOME` or ignored `local.properties`.
```powershell
.\gradlew.bat :scanner-host:testDebugUnitTest :scanner-host:assembleDebug :scanner-host:lintDebug
```
### Configuration
APK: `scanner-host/build/outputs/apk/debug/scanner-host-debug.apk`.
```kotlin
val client = QqqClient.Builder()
.baseUrl("https://your-qqq-app.com")
.build()
```
The existing libraries also have unit and emulator tests. `testDebugUnitTest` runs JVM tests; `connectedDebugAndroidTest` needs a connected device/emulator. Physical TC22 trigger, lifecycle and scanner-ownership behavior must still be verified on hardware.
### Basic Usage
See [emulator testing](docs/EMULATOR.md) for a local interactive demo, synthetic scanner broadcasts, and the disposable-emulator smoke check.
```kotlin
// Fetch records
val orders = client.query("order")
.filter("status", "pending")
.limit(20)
.execute()
## CI and delivery
// Create record
val newOrder = client.insert("order", mapOf(
"customerName" to "Acme Corp",
"total" to 100.00
))
The primary forge for source, reviews and checks is Gitea (`git.kof22.com/QRun-IO`); GitHub is a shadow/archive with no ordinary writes. The protected `main` branch on Gitea contains `.gitea/workflows/qrun-android-release.yaml`, which calls the shared `KofTwentyTwo/platform-ci` Android workflow (`android.yaml@qrun-release-v1`); that file is not in this feature working tree. Pull-request checks come from central Gitea policy, not from that release workflow. Legacy CircleCI and `.github` workflow files remain in the tree and are not all retired; the managed CircleCI profile still describes shared-library verification and its main-only Maven publication. Run the build commands above locally. See [emulator testing](docs/EMULATOR.md) for the isolated test package and contract checks, and [FINISH-1.0](docs/FINISH-1.0.md) for current progress.
// Run process
client.runProcess("orderSync")
```
Scanner APK signing and distribution are deliberately deferred and no distribution channel is selected; the retained CircleCI lane keeps its existing library publication; the `.github` scanner release automation is a candidate only (see [releasing](docs/RELEASING.md)). See [Contributing](CONTRIBUTING.md), [Security](SECURITY.md), and [Changelog](CHANGELOG.md).
## Usage
## Extension points
### Authentication
`LocalAuthenticatedContent` allows a host to gate authenticated QQQ content. `LocalCustomWidgetRenderer` lets the host render additional metadata-defined widgets without replacing the standard renderer. `NetworkQQQRepository` accepts a server-scoped header provider; credentials never enter process values.
```kotlin
val client = QqqClient.Builder()
.baseUrl("https://your-qqq-app.com")
.authToken("your-api-token")
.build()
```
### Querying Records
```kotlin
// With filters
val results = client.query("customer")
.filter("status", Operator.EQUALS, "active")
.filter("createdDate", Operator.GREATER_THAN, "2024-01-01")
.orderBy("name", SortOrder.ASC)
.execute()
// Get single record
val customer = client.get("customer", 123)
```
### Modifying Records
```kotlin
// Update
client.update("order", 123, mapOf(
"status" to "shipped"
))
// Delete
client.delete("order", 123)
```
## Project Status
Experimental. Core functionality is being developed.
### Roadmap
- Offline caching
- Background sync
- UI components for common patterns
- Kotlin coroutines support
## Contributing
1. Fork the repository
2. Create a feature branch
3. Submit a pull request
The scanner host supports TABLE_BASED authentication and server-advertised OAuth2 browser sign-in, including Authentik. Updated OAuth2 requires UI API1.10 native-session login and nonce-validation capabilities, the explicit REQUIRED nonce policy and callback registration; it is being qualified and does not infer support from the provider name. Unsupported servers cannot downgrade browser login; TABLE sign-in is unchanged. See [authentication setup and verification](docs/AUTHENTICATION.md). Existing shared-library Auth0 support remains intact.
## License
Apache-2.0 - See [LICENSE](LICENSE) for details.
Licensed under Apache-2.0; see [LICENSE](LICENSE) and [NOTICE](NOTICE). Historical project-owned source headers and the IDE copyright template have been reconciled with the repository's January 2026 license migration. Existing copyright ownership and third-party notices are preserved.
+9
View File
@@ -0,0 +1,9 @@
# Security policy
Do not open public issues containing vulnerabilities, credentials, device tokens, session cookies, or real scan data.
Report vulnerabilities privately through [GitHub security reporting](https://github.com/QRun-IO/qqq-android/security/advisories/new) when available, or email **security@qrun.io**. Include the affected commit/APK version, Android and QQQ versions, a synthetic reproduction, and the impact.
The scanner host is experimental. Report issues against the current development version; there is not yet a stable scanner APK support schedule. The broader [QQQ security policy](https://github.com/QRun-IO/qqq/blob/develop/SECURITY.md) applies to the framework.
Release installations must use HTTPS, authenticated users, and approved device credentials. The debug APK permits HTTP for local development only. Device enrollment secrets and sessions must not be included in backup, transfer, logs, or process values. Scanner-provided metadata is client input rather than proof of hardware identity.
+34 -1
View File
@@ -1,7 +1,40 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.aboutlibraries) apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0" apply false
}
}
/*******************************************************************************
** Keep every owned Kotlin variant and test compiler under the same warning
** policy; compatibility suppressions belong on the specific affected source.
*******************************************************************************/
subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions.allWarningsAsErrors = true
}
}
@@ -0,0 +1,12 @@
# Device Owner enrollment (destructive, separate from the safe profile)
Not generated and not part of `generate-company-profile.mjs`. Fully managed (Device Owner) provisioning is intended for a new or factory-reset handheld (`docs/DEVICE-PROVISIONING.md`, Android device management). Doing it wipes the device's data and settings, including the current scanner and printing setup.
Do not start unless all of these are recorded and approved by the owner of the device fleet:
1. The customer EMM/MDM, its enrollment method and Zebra OEMConfig package (`com.zebra.oemconfig.release` on Android 13+).
2. A spare device, not the development TC22, with the ordinary safe profile already reviewed.
3. The recovery route if the EMM or EHS locks the device out, and the factory-reset authority.
4. Confirmation that the QQQ device approval and Android management states stay separate.
Enrollment credentials, tokens and QR payloads belong in the EMM, never in this repository.
@@ -0,0 +1,230 @@
#!/usr/bin/env node
/* Copyright (c) 2026 QRunIO. SPDX-License-Identifier: Apache-2.0 */
// Renders a reviewable Zebra Enterprise Home Screen (EHS) config and, when the device is measured, a StageNow/MX
// AppMgr document from one non-secret JSON profile. Only elements documented by Zebra are emitted; see
// docs/COMPANY-PROFILE.md for the page citations. Nothing here installs, pushes or changes a device.
//
// Usage: node generate-company-profile.mjs <profile.json> <output-dir> [--require-ready]
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { pathToFileURL } from "node:url"
export const EHS_PACKAGE = "com.zebra.mdna.enterprisehomescreen"
export const EHS_ACTIVITY = "com.zebra.mdna.enterprisehomescreen.HomeScreenActivity"
const PACKAGE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/
const TITLE_TOKENS = new Set(["bluetooth_address", "bluetooth_name", "imei_number", "mac_address", "network_host_name", "serial_number", "device_name"])
const LOGO_EXTENSIONS = [".bmp", ".gif", ".jpg", ".png", ".webp"]
const APP_ICON_EXTENSIONS = [".bmp", ".jpg", ".png"]
const COLOR_NAMES = new Set(["red", "blue", "green", "black", "white", "gray", "cyan", "magenta", "yellow", "lightgray", "darkgray"])
// Keys and values consumed by the app's RestrictionsManager reader (scanner-host res/xml/app_restrictions.xml).
export const MANAGED_SERVER_KEY = "qqq_server_url"
export const MANAGED_ORIENTATION_KEY = "qqq_orientation"
export const MANAGED_ORIENTATIONS = ["SYSTEM", "PORTRAIT", "LANDSCAPE", "REVERSE_PORTRAIT", "REVERSE_LANDSCAPE"]
const BLOCKED_WILDCARDS =/^com\.(a|an|and|andr|andro|androi|android)\*?$/
/*******************************************************************************
** Mirror the app's normalizeServerUrl for managed values: HTTPS origin only. Returns the normalized origin or null.
*******************************************************************************/
export function managedServerUrl(value) {
if (typeof value !== "string" || value.length > 2048) return null
try {
const url = new URL(value.trim())
if (url.protocol !== "https:" || !url.hostname || url.username || url.password || url.search || url.hash || url.pathname !== "/") return null
return `https://${url.host}/`
} catch { return null }
}
/*******************************************************************************
** Escape the five characters EHS's XML parser requires to be escaped.
*******************************************************************************/
export function xml(value) {
return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;")
}
function devicePath(value, extensions, label, errors) {
if (value == null) return null
if (typeof value !== "string" || !value.startsWith("/enterprise/usr/") || value.split("/").includes("..") || !extensions.some((e) => value.toLowerCase().endsWith(e))) {
errors.push(`${label} must be an absolute /enterprise/usr/ path (persists across Enterprise Reset) ending in ${extensions.join(", ")}`)
return null
}
return value
}
function color(value, label, errors) {
if (value == null) return null
if (typeof value !== "string" || !(/^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(value) || COLOR_NAMES.has(value))) errors.push(`${label} must be #RRGGBB, #AARRGGBB or a documented color name`)
return value
}
function validateTitle(title, errors) {
if (typeof title !== "string" || title.trim() === "") { errors.push("launcher.title is required"); return { text: "", dynamic: false } }
const tokens = [...title.matchAll(/\[([^\]]*)\]/g)].map((m) => m[1])
for (const token of tokens) if (!TITLE_TOKENS.has(token)) errors.push(`launcher.title token [${token}] is not a documented device ID`)
if (new Set(tokens).size > 1 || tokens.length > 1) errors.push("launcher.title may contain at most one device ID token")
if (title.replace(/\[[^\]]*\]/g, "").match(/[\[\]]/)) errors.push("launcher.title square brackets are only allowed around a documented device ID")
return { text: title, dynamic: tokens.length === 1 }
}
function validateApp(app, index, list, errors, { icons }) {
const where = `${list}[${index}]`
if (!app || typeof app !== "object") { errors.push(`${where} must be an object`); return null }
const pkg = app.package
if (typeof pkg !== "string" || !PACKAGE.test(pkg)) {
errors.push(`${where}.package must be an exact package name (wildcards are rejected: least privilege)`)
if (typeof pkg === "string" && (pkg.includes("*") && BLOCKED_WILDCARDS.test(pkg))) errors.push(`${where}.package wildcard is blocked by EHS`)
return null
}
if (pkg === EHS_PACKAGE) errors.push(`${where} must not list EHS itself`)
if (typeof app.label !== "string" || app.label.trim() === "") errors.push(`${where}.label is required`)
else if (app.label.length > 18) errors.push(`${where}.label is longer than 18 characters; EHS truncates it`)
if (app.activity != null && (typeof app.activity !== "string" || !PACKAGE.test(app.activity))) errors.push(`${where}.activity must be a class name`)
const icon = icons ? devicePath(app.icon, APP_ICON_EXTENSIONS, `${where}.icon`, errors) : null
return { label: app.label, package: pkg, activity: app.activity ?? "", icon }
}
function applicationTag(app, indent) {
const icon = app.icon ? ` icon="${xml(app.icon)}"` : ""
return `${indent}<application label="${xml(app.label)}" package="${xml(app.package)}" activity="${xml(app.activity)}"${icon}/>`
}
/*******************************************************************************
** Validate a profile. Returns normalized values plus the inputs that are still missing before it can be installed.
*******************************************************************************/
export function validate(profile) {
const errors = []
const missing = []
const warnings = []
const p = profile ?? {}
if (typeof p.profileId !== "string" || !/^[a-z0-9][a-z0-9-]{1,62}$/.test(p.profileId)) errors.push("profileId must be lowercase letters, digits and hyphens")
if (!Number.isInteger(p.revision) || p.revision < 1) errors.push("revision must be a positive integer")
const device = p.device ?? {}
for (const key of ["androidVersion", "mxVersion", "osBuild"]) if (!device[key]) missing.push(`device.${key}: measure on the target device before generating installable MX output`)
if (!p.ehs?.installedVersion) missing.push("ehs.installedVersion: select an EHS build supported on the measured firmware (docs checked: 8.0)")
const q = p.qqq ?? {}
if (typeof q.package !== "string" || !PACKAGE.test(q.package)) errors.push("qqq.package must be the existing QQQ package name")
if (typeof q.activity !== "string" || !PACKAGE.test(q.activity)) errors.push("qqq.activity must be a fully qualified activity class")
if (typeof q.label !== "string" || q.label === "" || q.label.length > 18) errors.push("qqq.label is required (18 characters or fewer)")
const endpoint = managedServerUrl(q.endpoint)
if (endpoint === null) errors.push("qqq.endpoint must be an https origin with no credentials, path, query or fragment (the app rejects anything else)")
else if (new URL(endpoint).hostname.endsWith(".invalid")) missing.push("qqq.endpoint: replace the placeholder with the company QQQ server URL")
if (q.orientation != null && !MANAGED_ORIENTATIONS.includes(q.orientation)) errors.push(`qqq.orientation must be one of ${MANAGED_ORIENTATIONS.join(", ")} or omitted`)
const l = p.launcher ?? {}
const title = validateTitle(l.title, errors)
const logo = devicePath(l.titleBarLogo, LOGO_EXTENSIONS, "launcher.titleBarLogo", errors)
const wallpaper = devicePath(l.wallpaper, LOGO_EXTENSIONS, "launcher.wallpaper", errors)
if (!logo) missing.push("launcher.titleBarLogo: company logo file and its /enterprise/usr path")
if (!wallpaper) missing.push("launcher.wallpaper: company wallpaper file and its /enterprise/usr path")
const textColor = color(l.iconLabelTextColor ?? "#FFFFFFFF", "launcher.iconLabelTextColor", errors)
const backgroundColor = color(l.iconLabelBackgroundColor ?? "#00FFFFFF", "launcher.iconLabelBackgroundColor", errors)
if (l.iconSize != null) errors.push("launcher.iconSize is unsupported: Zebra's snippet does not show where user_options sits in the config file")
const apps = []
if (!Array.isArray(p.permittedApps) || p.permittedApps.length === 0) errors.push("permittedApps must list at least the QQQ application")
else p.permittedApps.forEach((a, i) => { const v = validateApp(a, i, "permittedApps", errors, { icons: true }); if (v) apps.push(v) })
if (!apps.some((a) => a.package === q.package)) errors.push("permittedApps must include the qqq.package application")
if (new Set(apps.map((a) => a.package)).size !== apps.length) errors.push("permittedApps contains a duplicate package")
if (apps.length === 1) missing.push("permittedApps: administrator list of required Android companion apps (camera/photo picker, printing); only QQQ is listed")
const tools = []
;(p.tools ?? []).forEach((a, i) => { const v = validateApp(a, i, "tools", errors, { icons: false }); if (v) tools.push(v) })
const r = p.restrictions ?? {}
for (const [key, allowed] of [["systemSettingsRestricted", [0, 1, 3]], ["recentAppsAccessDisabled", [0, 1]], ["recentAppsButtonDisabled", [0, 1, 2, 3]]]) {
if (!allowed.includes(r[key])) errors.push(`restrictions.${key} must be one of ${allowed.join(", ")} (explicit decision required)`)
}
if (r.systemSettingsRestricted === 3) warnings.push("systemSettingsRestricted 3 requires MX 11.7+ and OSx 11.5.21+ on the device")
if (r.systemSettingsRestricted === 1) warnings.push("systemSettingsRestricted 1 (EHS default) hides most Settings panels; confirm Bluetooth printer pairing and Wi-Fi workflows on a spare device")
const deploy = []
;(p.deploy ?? []).forEach((d, i) => {
const where = `deploy[${i}]`
const known = [EHS_PACKAGE, q.package]
if (!d || !known.includes(d.package)) errors.push(`${where}.package must be EHS or the QQQ package`)
else if (!["Install", "Upgrade"].includes(d.action)) errors.push(`${where}.action must be Install or Upgrade`)
else if (typeof d.apk !== "string" || !d.apk.startsWith("/") || d.apk.split("/").includes("..") || !d.apk.endsWith(".apk")) errors.push(`${where}.apk must be an absolute on-device .apk path already staged`)
else deploy.push({ package: d.package, action: d.action, apk: d.apk })
})
if (!deploy.some((d) => d.package === EHS_PACKAGE)) missing.push("deploy: EHS APK on-device path (artifact delivery method and checksum are an administrator decision)")
return { errors, missing, warnings, normalized: { title, logo, wallpaper, textColor, backgroundColor, apps, tools, deploy, qqq: q, launcher: l, restrictions: r } }
}
/*******************************************************************************
** Render enterprisehomescreen.xml. Tags not parameterized keep the values of Zebra's documented default file.
*******************************************************************************/
export function renderEhs(profile, n) {
const qqqApp = n.apps.find((a) => a.package === n.qqq.package)
const lines = ["<?xml version=\"1.0\" encoding=\"utf-8\"?>", `<!-- profile ${xml(profile.profileId)} revision ${profile.revision}; generated for review, see docs/COMPANY-PROFILE.md. EHS docs 8.0. -->`, "<config>"]
lines.push(" <kiosk>", applicationTag(qqqApp, " "), " </kiosk>")
lines.push(" <applications>", ...n.apps.map((a) => applicationTag(a, " ")), " </applications>")
if (n.tools.length > 0) lines.push(" <tools>", ...n.tools.map((a) => applicationTag(a, " ")), " </tools>")
// EHS stores a hash here when an administrator sets the password in Admin Mode; it must never be authored.
lines.push(" <passwords>", " <admin></admin>", " </passwords>")
lines.push(" <preferences>")
lines.push(n.title.dynamic ? ` <title><value>${xml(n.title.text)}</value></title>` : ` <title>${xml(n.title.text)}</title>`)
if (n.logo) {
lines.push(" <title_bar_icon>", " <title_bar_icon_disabled>0</title_bar_icon_disabled>", ` <title_bar_icon_file>${xml(n.logo)}</title_bar_icon_file>`)
lines.push(` <reuse_custom_icon>${n.launcher.reuseLogoOnEhsScreens ? 1 : 0}</reuse_custom_icon>`, " </title_bar_icon>")
}
lines.push(` <icon_label_background_color>${xml(n.backgroundColor)}</icon_label_background_color>`, ` <icon_label_text_color>${xml(n.textColor)}</icon_label_text_color>`)
lines.push(" <orientation></orientation>", " <auto_launch_enable>0</auto_launch_enable>", " <service_auto_launch_enable>0</service_auto_launch_enable>")
lines.push(` <wallpaper>${xml(n.wallpaper ?? "")}</wallpaper>`)
if (n.wallpaper) lines.push(` <wallpaper_stretching_enabled>${n.launcher.wallpaperStretch ? 1 : 0}</wallpaper_stretching_enabled>`)
lines.push(" <kiosk_mode_enabled>0</kiosk_mode_enabled>", " <keyguard_camera_disabled>1</keyguard_camera_disabled>", " <keyguard_search_disabled>1</keyguard_search_disabled>")
lines.push(" <usb_debugging_disabled>1</usb_debugging_disabled>", ` <system_settings_restricted>${n.restrictions.systemSettingsRestricted}</system_settings_restricted>`)
lines.push(` <recent_apps_button_disabled>${n.restrictions.recentAppsButtonDisabled}</recent_apps_button_disabled>`, ` <recent_apps_access_disabled>${n.restrictions.recentAppsAccessDisabled}</recent_apps_access_disabled>`)
lines.push(" <wireless_info>", ` <show_ip_address>${profile.launcher?.showIpAddress ? 1 : 0}</show_ip_address>`, " <show_mac_address>0</show_mac_address>", " <show_bssid>0</show_bssid>", " <show_ssid>0</show_ssid>", " <show_scan_result>0</show_scan_result>", " </wireless_info>")
lines.push(" <screen_blanking>", " <blanking_enabled>0</blanking_enabled>", " <speed_threshold>10</speed_threshold>", " <blanking_mode>1</blanking_mode>", " </screen_blanking>")
lines.push(" </preferences>")
lines.push("</config>", "")
return lines.join("\n")
}
/*******************************************************************************
** Render the StageNow/MX AppMgr document: optional APK installs, then EHS as the default launcher.
*******************************************************************************/
export function renderMx(profile, n) {
const lines = ["<?xml version=\"1.0\" encoding=\"utf-8\"?>", `<!-- profile ${xml(profile.profileId)} revision ${profile.revision}; AppMgr only. Device ${xml(profile.device.model)} Android ${xml(profile.device.androidVersion)} MX ${xml(profile.device.mxVersion)}. -->`, "<wap-provisioningdoc>"]
for (const d of n.deploy) lines.push(" <characteristic version=\"4.2\" type=\"AppMgr\">", ` <parm name="Action" value="${d.action}" />`, ` <parm name="APK" value="${xml(d.apk)}" />`, " </characteristic>")
lines.push(" <characteristic version=\"4.2\" type=\"AppMgr\">", " <parm name=\"Action\" value=\"SetDefaultLauncher\" />", ` <parm name="Package" value="${EHS_PACKAGE}" />`, " </characteristic>")
lines.push("</wap-provisioningdoc>", "")
return lines.join("\n")
}
/*******************************************************************************
** Validate then write outputs. MX output is withheld until the device is measured.
*******************************************************************************/
export function generate(profile, outDir) {
const { errors, missing, warnings, normalized } = validate(profile)
const report = { profileId: profile?.profileId, revision: profile?.revision, ready: false, errors, missing, warnings, files: [] }
if (errors.length > 0) return report
mkdirSync(outDir, { recursive: true })
const emit = (name, text) => { writeFileSync(join(outDir, name), text, "utf8"); report.files.push(name) }
emit("enterprisehomescreen.xml", renderEhs(profile, normalized))
// Android managed configuration for the EMM; withheld while the endpoint is still a placeholder.
if (!missing.some((m) => m.startsWith("qqq.endpoint"))) {
const managed = { [MANAGED_SERVER_KEY]: managedServerUrl(profile.qqq.endpoint) }
if (profile.qqq.orientation != null) managed[MANAGED_ORIENTATION_KEY] = profile.qqq.orientation
emit("managed-configuration.json", JSON.stringify(managed, null, 2) + "\n")
} else report.warnings.push("managed-configuration.json withheld: qqq.endpoint is still a placeholder")
const mxReady =!missing.some((m) => m.startsWith("device.") || m.startsWith("deploy:"))
if (mxReady) emit("stagenow-mx-appmgr.xml", renderMx(profile, normalized))
else report.warnings.push("stagenow-mx-appmgr.xml withheld: device measurements and EHS APK path are missing")
report.ready = missing.length === 0
writeFileSync(join(outDir, "profile-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8")
report.files.push("profile-report.json")
return report
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
const [input, outDir, ...flags] = process.argv.slice(2)
if (!input || !outDir) { console.error("usage: generate-company-profile.mjs <profile.json> <output-dir> [--require-ready]"); process.exit(64) }
const report = generate(JSON.parse(readFileSync(input, "utf8")), outDir)
console.log(JSON.stringify(report, null, 2))
if (report.errors.length > 0) process.exit(1)
if (flags.includes("--require-ready") && !report.ready) process.exit(2)
}
@@ -0,0 +1,125 @@
/* Copyright (c) 2026 QRunIO. SPDX-License-Identifier: Apache-2.0 */
import assert from "node:assert/strict"
import { mkdtempSync, readFileSync, readdirSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import test from "node:test"
import { EHS_PACKAGE, MANAGED_ORIENTATIONS, generate, managedServerUrl, validate, xml } from "./generate-company-profile.mjs"
const example = () => JSON.parse(readFileSync(new URL("./profile.example.json", import.meta.url), "utf8"))
const measured = () => {
const p = example()
Object.assign(p.device, { androidVersion: "14", mxVersion: "14.0.0.0", osBuild: "measured-build" })
p.ehs.installedVersion = "8.0"
p.qqq.endpoint = "https://qqq.company.test"
p.permittedApps.push({ label: "Camera", package: "com.example.camera", activity: null, icon: null })
p.deploy = [{ package: EHS_PACKAGE, action: "Install", apk: "/enterprise/usr/persist/EHS.apk" }]
return p
}
const out = () => mkdtempSync(join(tmpdir(), "company-profile-"))
test("example is valid but reports every unmeasured input and withholds MX output", () => {
const dir = out()
const report = generate(example(), dir)
assert.deepEqual(report.errors, [])
assert.equal(report.ready, false)
assert.ok(report.missing.some((m) => m.startsWith("device.mxVersion")))
assert.ok(report.missing.some((m) => m.startsWith("permittedApps")))
assert.ok(report.missing.some((m) => m.startsWith("qqq.endpoint")))
assert.deepEqual(readdirSync(dir).sort(), ["enterprisehomescreen.xml", "profile-report.json"])
})
test("measured profile is ready and emits documented EHS and MX elements only", () => {
const dir = out()
const report = generate(measured(), dir)
assert.deepEqual(report.errors, [])
assert.equal(report.ready, true)
const ehs = readFileSync(join(dir, "enterprisehomescreen.xml"), "utf8")
assert.match(ehs, /<title><value>Company \[serial_number\]<\/value><\/title>/)
assert.match(ehs, /<wallpaper>\/enterprise\/usr\/company\/wallpaper\.png<\/wallpaper>/)
assert.match(ehs, /<usb_debugging_disabled>1<\/usb_debugging_disabled>/)
assert.match(ehs, /<kiosk_mode_enabled>0<\/kiosk_mode_enabled>/)
assert.match(ehs, /<admin><\/admin>/)
const mx = readFileSync(join(dir, "stagenow-mx-appmgr.xml"), "utf8")
assert.match(mx, /value="SetDefaultLauncher"/)
assert.match(mx, new RegExp(`name="Package" value="${EHS_PACKAGE.replace(/\./g, "\\.")}"`))
assert.doesNotMatch(ehs, /<admin>[^<]+<\/admin>/)
assert.doesNotMatch(mx + ehs, /token|secret/i)
})
test("security-weakening or undocumented inputs are rejected", () => {
const cases = [
(p) => { p.permittedApps.push({ label: "All", package: "com.android.*" }) },
(p) => { p.permittedApps.push({ label: "Wild", package: "com.mobility.*" }) },
(p) => { p.launcher.wallpaper = "/sdcard/wallpaper.png" },
(p) => { p.launcher.wallpaper = "/enterprise/usr/../x.png" },
(p) => { p.qqq.endpoint = "http://qqq.company.test" },
(p) => { p.qqq.endpoint = "https://user:pw@qqq.company.test" },
(p) => { p.launcher.title = "Dynamic [username]" },
(p) => { p.restrictions.systemSettingsRestricted = 2 },
(p) => { delete p.restrictions.recentAppsAccessDisabled },
(p) => { p.launcher.iconSize = "XL" },
(p) => { p.permittedApps.push({ label: "A very long label over limit", package: "com.example.long" }) },
(p) => { p.permittedApps = [{ label: "Other", package: "com.example.other" }] },
(p) => { p.deploy = [{ package: "com.evil.app", action: "Install", apk: "/enterprise/usr/persist/x.apk" }] },
]
for (const mutate of cases) {
const p = measured()
mutate(p)
assert.ok(validate(p).errors.length > 0, mutate.toString())
}
})
test("managed configuration uses the app's exact keys and is withheld for a placeholder endpoint", () => {
const withheld = out()
const placeholder = generate(example(), withheld)
assert.ok(!readdirSync(withheld).includes("managed-configuration.json"))
assert.ok(placeholder.warnings.some((w) => w.startsWith("managed-configuration.json withheld")))
const dir = out()
const p = measured()
p.qqq.endpoint = " https://QQQ.Company.test "
p.qqq.orientation = "REVERSE_LANDSCAPE"
assert.deepEqual(generate(p, dir).errors, [])
assert.deepEqual(JSON.parse(readFileSync(join(dir, "managed-configuration.json"), "utf8")), {
qqq_server_url: "https://qqq.company.test/",
qqq_orientation: "REVERSE_LANDSCAPE",
})
const noOrientation = out()
generate(measured(), noOrientation)
assert.deepEqual(Object.keys(JSON.parse(readFileSync(join(noOrientation, "managed-configuration.json"), "utf8"))), ["qqq_server_url"])
})
test("managed server URL accepts only an https origin, mirroring the app", () => {
assert.equal(managedServerUrl("https://qqq.company.test"), "https://qqq.company.test/")
assert.equal(managedServerUrl("https://qqq.company.test:8443/"), "https://qqq.company.test:8443/")
for (const bad of ["http://qqq.company.test", "https://u:p@qqq.company.test", "https://qqq.company.test/app", "https://qqq.company.test/?x=1", "https://qqq.company.test/#f", "ftp://qqq.company.test", "nonsense", "", null, 7, `https://${"a".repeat(2100)}`]) {
assert.equal(managedServerUrl(bad), null, String(bad))
const p = measured()
p.qqq.endpoint = bad
assert.ok(validate(p).errors.length > 0, String(bad))
}
})
test("orientation must be an exact app value", () => {
for (const good of MANAGED_ORIENTATIONS) {
const p = measured()
p.qqq.orientation = good
assert.deepEqual(validate(p).errors, [])
}
for (const bad of ["portrait", "Landscape", "AUTO", "SENSOR", "", 90]) {
const p = measured()
p.qqq.orientation = bad
assert.ok(validate(p).errors.length > 0, String(bad))
}
})
test("XML special characters in labels are escaped", () => {
assert.equal(xml(`a&b<c>"d"'e'`), "a&amp;b&lt;c&gt;&quot;d&quot;&apos;e&apos;")
const p = measured()
p.permittedApps[1].label = "Pick & <Scan>"
const dir = out()
assert.deepEqual(generate(p, dir).errors, [])
assert.match(readFileSync(join(dir, "enterprisehomescreen.xml"), "utf8"), /label="Pick &amp; &lt;Scan&gt;"/)
})
@@ -0,0 +1,41 @@
{
"profileId": "example-company",
"revision": 1,
"device": {
"model": "TC22",
"androidVersion": null,
"mxVersion": null,
"osBuild": null
},
"ehs": {
"docsVersion": "8.0",
"installedVersion": null
},
"qqq": {
"package": "io.qrun.mobile.scanner",
"activity": "io.qrun.mobile.scanner.ScannerActivity",
"label": "QQQ Mobile",
"endpoint": "https://qqq.example.invalid",
"orientation": null
},
"launcher": {
"title": "Company [serial_number]",
"titleBarLogo": "/enterprise/usr/company/logo.png",
"reuseLogoOnEhsScreens": true,
"wallpaper": "/enterprise/usr/company/wallpaper.png",
"wallpaperStretch": false,
"iconLabelTextColor": "#FFFFFFFF",
"iconLabelBackgroundColor": "#00FFFFFF",
"iconSize": null
},
"permittedApps": [
{ "label": "QQQ Mobile", "package": "io.qrun.mobile.scanner", "activity": "io.qrun.mobile.scanner.ScannerActivity", "icon": null }
],
"tools": [],
"restrictions": {
"systemSettingsRestricted": 1,
"recentAppsAccessDisabled": 1,
"recentAppsButtonDisabled": 1
},
"deploy": []
}
+17
View File
@@ -0,0 +1,17 @@
# Optional whole-device screen viewing
Status: accepted implementation scope; validation pending.
The existing mobile-scanners 1.1 transport captures only eligible QQQ app content. Its foreground/focus checks and PixelCopy implementation remain the default. Whole-device viewing needs an explicit semantic mode because the old server deliberately rejects background capture and its schemas prohibit other-app/OS pixels.
The additive 1.2 contract advertises supported capture modes in the session heartbeat. Missing capabilities and missing mode fields mean `APP_CONTENT`. `DEVICE_SCREEN` is selected only by an authenticated, approved device operator using Android's own consent dialog. An administrator cannot grant that consent. The existing admin-only viewer and current device/session authorization remain mandatory; a mode change invalidates old leases and pixels. Each new lease and frame carries its mode, preventing a full-display frame from being accepted as app-content capture. The session record exposes the current mode to the administrator.
Android uses a nonexported `mediaProjection` foreground service, an ongoing informational notification, one in-memory consent result, one virtual display and a bounded ImageReader. No audio, remote control, screen recording or saved screenshots are added. Frame size/rate/byte limits and ephemeral latest-only server retention stay unchanged. A single MobileSessionController owns heartbeats/uploads; device mode changes its capture source and allows background heartbeats while consent and the current authenticated session remain valid. Location telemetry retains its existing foreground-only rules.
Starting requires an explicit operator action and system approval for this sharing session, plus an enabled sharing notification. While waiting for an administrator's bounded lease, no frame is uploaded; absence of a lease times out after 30 seconds. Once viewing has begun, lease loss/expiry, network failure, system Stop, device lock, approval loss, logout, server change or controller disposal invalidates capture immediately and releases projection/display/images/service. A service watchdog independently enforces the lease deadline even during encoding or an outstanding request. There is no automatic restart or remembered consent. Activity recreation may end sharing and require fresh consent; it must never silently broaden or revive a grant. System Stop suspends viewing until a fresh sharing grant is started. The app has no Stop viewing, Allow viewing, or custom notification stop action. Android system consent and stop controls remain available.
Implementation files: Android SessionScreen/MobileSessionApi/MobileSessionController, new DeviceScreenCaptureService and operator control, ScannerActivity wiring and manifest; server MobileSessionService/MobileScreenService/MobileSessionSchemas/ScreenSpec, session metadata/H2 additive migration and extension version. Existing standard rendering and authenticated session APIs are reused; no parallel media server or business-screen framework is added.
Required tests cover old1.1 fallback, mode mismatch/lease rotation, revoked/ended sessions, bounded no-viewer timeout, late consent/capture after logout, denial/cancel/duplicate result, stale frame disposal, system Stop. Actual Android consent, background display capture, rotation/lock and protected-window behavior require a separate approved native test; synthetic tests do not establish physical capture acceptance.
Android's [official MediaProjection guide](https://developer.android.com/media/grow/media-projection) requires new consent for each projection session, one-use projection tokens and callback cleanup. Android14+ requires the mediaProjection foreground-service type/permission and supports display or app selection. The device-screen control requests the default display where supported and keeps the system confirmation visible. Protected windows remain protected by Android; we do not attempt to bypass them. A consenting user may expose other apps and notifications, which the control must explain before starting.
+96
View File
@@ -0,0 +1,96 @@
# Scanner authentication
The server selects the login provider through the versioned QQQ UI API. `TABLE_BASED` uses the native username/password form. `OAUTH2` uses the organization's external browser only when the server explicitly advertises the supported native session flow. This supports Authentik without embedding an Authentik URL or client secret in a customer-specific APK. Unknown providers and unsupported flows show a configuration message; OAuth cancellation never falls back to a local password form.
The shared scanner APK accepts only `TABLE_BASED` and the API 1.10 `OAUTH2` flow described here. Legacy `AUTH0` support belongs to the shared library and sample host; the scanner rejects that metadata before opening a browser. Its unused Auth0 manifest placeholders do not configure a customer login. Legacy callback lifecycle tests do not establish a live Auth0 provider round trip. This matches the [README's scanner support scope](../README.md).
## Native browser contract
Before opening a browser, the updated client requires UI API `>=1.10.0 <2.0.0`, both `nativeSessionLogin` and `nativeSessionNonceValidation` features, and this typed descriptor in `/qqq/v1/metaData/authentication`. Missing or unknown nonce policy is unsupported; there is no legacy browser-login downgrade. TABLE sign-in is unchanged:
```json
{
"type": "OAUTH2",
"name": "organization",
"values": {
"clientId": "server-exchanged-client",
"scopes": "openid profile email",
"nativeSessionLogin": {
"flow": "AUTHORIZATION_CODE_PKCE_SESSION",
"codeChallengeMethod": "S256",
"prompt": "login",
"nonceValidation": "REQUIRED",
"issuer": "https://identity.example/application/o/company/",
"discoveryUrl": "https://identity.example/application/o/company/.well-known/openid-configuration",
"redirectUris": ["io.qrun.mobile.scanner.oauth://callback/oauth2redirect"]
}
}
}
```
Values above are illustrative. The actual server advertises its issuer, discovery URL, client and scopes. In Authentik global issuer mode, `issuer` may be the host root while `discoveryUrl` remains under the application path. The client does not guess either value. Server, issuer, discovery and discovered authorization/token endpoints require HTTPS for this flow, including on debug builds. Existing local TABLE_BASED testing may still use its debug HTTP policy.
AppAuth 0.11.1 opens an external browser with authorization code, query response mode, random state and nonce, S256 PKCE, and the advertised `prompt=login` policy. Missing or other prompt policies are unsupported. This asks the provider to authenticate the operator again; provider behavior must be qualified rather than assuming the request prevents silent SSO reuse. The client validates the discovery issuer and exact callback, then submits `authenticationFlow`, `code`, `codeVerifier`, `redirectUri` and `expectedNonce` to the existing `/qqq/v1/manageSession`. QQQ exchanges the code using its server-held secret and establishes its normal session only after the configured token/nonce checks. The optional `nativeClientId` metadata describes a separate public-client bearer flow and is never substituted in this stored-session exchange.
The nonce comes only from the original in-memory pending attempt, never from callback input. It must be 16–512 unpadded base64url characters (`A–Z`, `a–z`, `0–9`, `_`, `-`). The captured nonce policy is part of the attempt binding. A missing/mismatched callback request nonce consumes and rejects that attempt; server rejection never triggers a retry without `expectedNonce`. Other authentication DTO uses omit this optional field. The client does not receive an ID token during login and does not validate its claims itself: the advertised server policy must validate the authenticated token-endpoint response and expected nonce before creating a principal/session. Actual server/provider nonce enforcement remains a separate acceptance gate; earlier fixture evidence with `nonceValidationTested:false` is not upgraded by these client changes.
## Deployment setup
An administrator must configure the native session descriptor in QQQ and register the exact corresponding callback in the existing identity provider's allowed redirect list:
| Installed package | Callback |
| --- | --- |
| Normal scanner | `io.qrun.mobile.scanner.oauth://callback/oauth2redirect` |
| Isolated E2E scanner | `io.qrun.mobile.scanner.e2e.oauth://callback/oauth2redirect` |
Each customer uses the shared normal callback; branding remains server-driven. Register only packages actually deployed. Do not use wildcards or Authentik's first-use redirect registration. The provider must permit authorization code with S256 PKCE for the configured server-exchanged client. Keep its secret in existing server secret management, never in APK resources, client metadata or documentation. Do not register the E2E callback on production merely to run tests.
## Lifecycle and privacy
The pending attempt is kept in memory and bound to server, provider metadata, client, scopes, prompt and callback. Rotation preserves it; process death requires starting again. Attempts expire after five minutes and are consumed once. Server change, logout, cancellation and reset invalidate the attempt. A late exchange cookie is never automatically adopted by the HTTP interceptor. If an abandoned response provides a new session identifier, the client attempts a bounded logout at its original server without replacing the current session. If a network failure prevents receiving that identifier, server expiry remains the recovery boundary.
Provider callback URLs, authorization codes, verifiers and raw errors are not shown in diagnostics. App sign-out ends the QQQ session. The browser's identity-provider SSO session may remain; global provider logout is not implied by this contract.
Password and legacy Auth0 flows also bind each attempt and provisional cookie to
its initiating server, metadata and generation. A response cannot restore a
signed-out operator or replace a later operator after a server change. The
repository returns provisional session results: custom callers must validate
their pending attempt before explicitly adopting the session. Repeated logout
effects share one logical sign-out; local credentials are cleared independently
of bounded server revocation and the browser provider's outcome.
The fresh-login prompt is a request to the provider, not cryptographic proof of a newly validated `auth_time`. Before shared-handheld deployment, test with the prior operator still signed into the browser: the next login must require authentication, allow the next operator to identify themselves, and attribute subsequent scans to that operator. Custom provider flows or upstream SSO may affect this behavior and require provider configuration; this client change does not modify them.
## Optional company sign-out (implementation awaiting qualification)
Ordinary **Sign out** continues to end only the QQQ session. The scanner also exposes a separate **Sign out of company** action in Account when the server advertises API1.10 or later within major1, feature `nativeSessionLogout`, and OAuth2 metadata `nativeSessionLogout` with flow `OIDC_RP_INITIATED_LOGOUT` and `postLogoutRedirectUris`. Other library hosts opt into that separate action with `LocalCompanySignOutEnabled`; it is disabled by default. No automatic provider-logout policy is selected. The confirmation explains that company sign-out may affect other applications in the browser.
Register the installed package's exact callback separately as a provider **post-logout** redirect, even when it matches the existing login callback. The client sends `{"nativeLogout":{"postLogoutRedirectUri":"<registered callback>"}}` to the existing `POST /qqq/v1/logout`. Its optional `nativeLogoutData` contains `issuer`, `endSessionEndpoint`, `postLogoutRedirectUri` and sensitive `idTokenHint`. Old sessions or unsupported servers may legitimately omit this data. The ordinary logout request remains unchanged.
The client clears QQQ session/private process state before opening AppAuth's end-session browser. It independently fetches discovery from the original login binding and checks the issuer, HTTPS end-session endpoint (without user information, query or fragment), and exact callback. A distinct in-memory, single-use state attempt binds completion to server, provider and endpoint. Login cannot start during company logout; cancellation, wrong state, reset or a late callback cannot restore a QQQ session. Browser cancellation or unavailable data explicitly reports incomplete company sign-out. Returning from the provider is protocol completion, not an independent claim about every upstream SSO session.
The hint is deliberately held in transient ordinary classes with no generated value `toString`, never application preferences, ViewModel state, diagnostics, process values or application saved state. Parsing failures and HTTP diagnostics do not expose response bodies. AppAuth0.11.1 necessarily serializes its end-session request into Android Intents/saved Activity state, and the browser receives the hint in a URL query. This is not a zero-persistence guarantee. Do not enable URI/body logging, browser traces or proxy access logs containing query strings. AppAuth's warning on a state mismatch can contain state values; do not treat SDK debug output as a safe diagnostics feed. Native synthetic tests include a hint logcat/preferences canary; real-provider and hardware qualification are separate gates.
The real Authentik2026.8.5 desktop test established A's principal and QQQ logout invalidation, but a subsequent `prompt=login` request silently reused A's retained provider SSO. That failed freshness assertion remains valid evidence; the optional RP logout flow must pass its own actual A → company sign-out → fresh B acceptance before shared-handheld deployment. It does not patch provider internals or clear browser cookies in a test.
## Host storage and transport
Applications embedding the shared libraries must keep reusable session storage under
Android's `noBackupFilesDir` and exclude it from both cloud backup and device transfer.
Ordinary preference storage can carry session authority to a restored installation.
The scanner and sample hosts disable backup and define explicit transfer exclusions.
The sample now uses a no-backup store; it deliberately does not migrate its previous
backup-eligible settings, so an upgraded sample requires a fresh login.
The sample permits HTTP development environments and cleartext traffic only in debug
builds. Its release manifest rejects cleartext even for an old restored HTTP address.
Embedding applications must enforce the same release transport policy before allowing
authentication. The scanner's existing release HTTPS requirement remains in force.
## Verification status
The latest security qualification passes 109 JVM tests, scanner lint and APK assembly. All 32 selected native navigation/input/account/authentication/storage cases pass on TC22, including seven synthetic AppAuth request/result scenarios and ten additional authentication lifecycle regressions. See [the scoped security evidence](SECURITY-REVIEW-2026-10-31.md) and [native OAuth evidence](NATIVE-AUTH-ACCEPTANCE.md) for their boundaries. Earlier generated AppAuth notices were verified in debug/E2E APKs. A real configured Authentik sign-in remains a separate acceptance gate: it requires a reachable HTTPS QQQ server advertising the descriptor, an explicitly registered callback and an authorized test account. No live provider configuration was changed as part of implementation.
Primary references: [AppAuth Android 0.11.1](https://github.com/openid/AppAuth-Android/tree/0.11.1) documents external-browser authorization, PKCE and server-side exchange when a confidential secret is needed. [Authentik OAuth2 provider documentation](https://docs.goauthentik.io/add-secure-apps/providers/oauth2/) documents PKCE, exact redirect registration and global issuer mode.
Authentik's [version 2026.8.3 authorization implementation](https://github.com/goauthentik/authentik/blob/version/2026.8.3/authentik/providers/oauth2/views/authorize.py#L550-L560) handles `prompt=login` by requiring a new login event. Its [current ForceAuthn documentation](https://docs.goauthentik.io/add-secure-apps/providers/saml/#forceauthn) describes the analogous authentication-flow behavior. Actual configured-provider acceptance remains required.
+15
View File
@@ -0,0 +1,15 @@
# Hosted scanner fixture
The source pins and complete Maven commands live in [Scanner Android](../.github/workflows/scanner.yml). The fixture uses core commit `ecf0f550a0357cec6387c2b54efca128a405a629` and scanner server commit `10416938724c9663d66f7848ff2b1a63fe635794`. The core includes the UI API 1.9 native authentication contract, credential log redaction, explicit JSON null handling, the Guava 33.7.2-jre security update, portable SPA filesystem paths, and automatically allocated spec-test ports. The scanner includes the canonical UTC H2 migration, the demo BOM import that aligns all transitive QQQ dependencies with the selected version, and the additive mobile-scanners1.2 capture-mode contract. The normal operator server remains a separately qualified runtime; a CI source pin does not replace it.
Each job creates a fresh Maven repository under the runner's temporary directory. Core is built as `4.1.0-ci.<full-core-SHA>`; the scanner and demo receive that exact version through `qqq.version`. This 49-character value retains the full commit identity and fits the core's 50-character help-bundle version limit. Nothing installs into the runner's shared Maven repository, substitutes a published artifact, or restores a cached QQQ snapshot. The scanner's own fixture snapshot is built from its checked source in that same fresh repository. This is an isolated CI source qualification, not publication of a production release or adoption by an operator server. The optional Record Files integration has its own dependency qualification.
The core reactor includes the BOM and middleware API with their required upstream modules. It runs every test in that dependency closure, Checkstyle, JaCoCo coverage checks, SpotBugs and PMD through the full `clean install` lifecycle. The scanner and demo then run their full verification suites before Maven writes the demo's runtime classpath, dependency tree and effective POM. The standard `jar` tool and Node SHA-256 implementation inspect that resolved classpath: every runtime JAR must belong to the private repository, every backend QQQ module must use the unique core version, required modules must exist, exactly H2 2.2.224 and Guava 33.7.2-jre must occur, and QQQ-owned JARs must not duplicate backend classes. The pinned frontend distribution keeps its independent version.
`fixture-provenance.json` records all resolved runtime JAR hashes, both source commits, the core revision and the unique QQQ class count. The dependency tree, effective POM and test evidence accompany the emulator artifacts. These checks inspect the selected runtime closure; they do not promise reproducible byte-for-byte Java builds or validate a published release.
For a local reproduction, check out both exact commits into disposable source directories and run the workflow's Maven commands with a new empty private repository path. Use Java 21, the checked-in wrappers, two active processors and a 768 MiB heap. Windows PowerShell uses `mvnw.cmd` and quoted `-D` arguments. Never point `maven.repo.local` at the shared `.m2` repository or reuse an operator database. Run `verify-fixture-provenance.mjs` with the server directory, private repository path, core SHA and server SHA, then run the emulator entry point as described in [EMULATOR.md](EMULATOR.md).
The local Windows qualification adds `-Dexec.skip=true` only to the core command because Maven's inline `sh -c` coverage-summary formatting fails Windows argument quoting. The inspected dependency closure has only that cosmetic exec-plugin execution. This exception retains all tests, coverage checks and static analyses; the hosted Ubuntu command keeps the summary enabled. Do not use a focused test selector with the full lifecycle: its coverage thresholds apply to the complete module suites.
Preserve the full native runner. In particular, `admin-forms` submits an actual optional null reason through the native session-ending form and guards the prior HTTP 500 failure. `session-monitor` separately verifies screen viewing, remote termination and fresh login; its administrative helper alone does not exercise that nullable form. TABLE_BASED fixture metadata must not advertise OAuth2-only native login support. Actual configured identity-provider acceptance remains a separate gate.
+43
View File
@@ -0,0 +1,43 @@
# Claude native handoff: Echo setup named possible values
## Source-verified contract (core `qqq-wt-native-provider-logout`)
- operationId `searchProcessPossibleValues`: `POST /qqq/v1/processes/{processName}/possibleValues/{fieldName}` (`UiApiOperationIds`, `ProcessPossibleValuesSpecV1`, openapi.yaml ~12440). Server advertises feature `possibleValues` on `GET /version`.
- Request: `application/json` `{ "values": {other form values}, "processValues": {...}, "useCase": "form", "searchTerm"?, "ids"? }` (`PossibleValuesSpecUtils`). Not multipart. Same session cookie, `X-QQQ-Client` and device headers as other native calls (shared Retrofit client).
- Response `PossibleValuesResponseV1`: `{ "options": [{ "id", "label", ... }], "hasMore"? }`; `options` is omitted when empty.
- Field metadata (FieldMetaData v1): `possibleValueSourceName`, and for contingent fields `possibleValueSourceDependsOn: [names]` and `possibleValueSourceDependencyBehavior: DISABLE_UNTIL_SET | SHOW_ALL_UNTIL_SET` (docs/CONTINGENT-POSSIBLE-VALUES.md).
- Frontend rules (same doc): re-query when a dependency changes, clear a selection that no longer fits (transitively), disable with a hint until dependencies are set (default), ignore stale responses.
A previous revision of this work invented the multipart shape from memory; it has been replaced entirely by the above. The route path happened to match; the body did not.
## Root cause (unchanged, source inference)
Echo `setup` declares `printerId`/`mediaId` with named PVS (media `dependsOn printerId`); fixture uses ENUM PVS. The client rendered only inline choices, so Printer became a free-text "Printer *" field and submission was blocked as unsupported. That init is a synchronous COMPLETE (not ASYNC) is inferred from sources and the operations log; the native E2E remains the authority. The fixture is NOT changed to inline choices.
## Implementation
- core: `QQQApiService.processPossibleValues`; `QQQRepository.processPossibleValues(process, field, values, processValues)` (default throws Unsupported so other fakes compile); `NetworkQQQRepository` builds the JSON body and decodes options (scalar ids kept); `QFieldMetaData` gains `possibleValueSourceDependsOn` / `possibleValueSourceDependencyBehavior`.
- mobileapp `ProcessViewModel`: `loadRemoteOptions`, `remoteOptionsBlockedReason` (DISABLE_UNTIL_SET -> "Choose Printer first." and no request), `fieldChanged` (dependents drop selection immediately, transitively, and re-query), per-field generation token so only the newest request publishes (a late reply for an old parent cannot restore options or a selection), post-load prune of a choice no longer offered, `fieldResets` so the control follows a VM-cleared value. Load failures are shown with "Retry choices", cancel the superseded job, never set client-work, so Cancel/Leave stay available. `reset()` clears all of it.
- `ProcessComponentEditForm`: select for named PVS (label text unchanged: "Printer", "Paper or label size", button "Choose a value"), disabled with a hint while blocked.
- `SemanticProcess.unsupported` accepts named PVS in EDIT_FORM only (widget INPUT_FIELD remains unsupported).
- E2E test: literal `waitFor("Printer")` unchanged (diagnostic text dump retained on timeout); added a readiness wait for the enabled media control before clicking it, since media is contingent. No assertion weakened.
- docs/UI-API.md updated.
## Tests added/updated
- `NetworkQQQRepositoryTest.processPossibleValuesUsesThePublishedJsonOperation`: method/path/JSON body/use case, response with options, and the omitted-`options` empty response.
- `ProcessSubmissionTest.namedPossibleValueSourcesLoadWithDependentValuesAndReportFailures`: blocked dependent not searched; parent value sent; dependent selection cleared on parent change; late old-parent reply ignored; failure reported, no client-work block, retry succeeds.
- `SemanticProcessTest`: named PVS in EDIT_FORM supported; UNKNOWN type and widget PVS still unsupported.
Note: server response bodies in tests are minimal shapes taken from `PossibleValuesResponseV1`/`ProcessPossibleValuesSpecV1Test` (`options[].id/label`), not captured from a live server.
## Commands for root (Java 21, wrapper)
```
./gradlew :qqq:android:core:testDebugUnitTest :qqq:android:mobileapp:testDebugUnitTest :scanner-host:testDebugUnitTest :scanner-host:lintDebug :scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest
node --test scripts/ci/scanner-echo-guards.test.mjs
```
Then re-approve rebuilt APK hashes and run the root native Echo helper.
## Known gaps / risks
- Nothing compiled or run (no shell). Expect possible small compile fixes (Compose state/`mutableIntStateOf` delegate, imports).
- `possibleValues` is not added to the client's REQUIRED_FEATURES (all existing fixtures advertise only six features); a server lacking it fails the search with a visible error + Retry rather than at startup.
- Pre-existing and untested: decoding inline `inlinePossibleValueSource` ids typed `java.io.Serializable` via kotlinx.
- Whether the real Echo metadata publishes `possibleValueSourceDependsOn` for media is inferred from `withPossibleValueSourceDependsOn(List.of("printerId"))` in the producer; unverified against a live `/metaData/process/echoPrint` response.
- Untracked `native.md` at the repo root is an obsolete earlier handoff (a pointer to this file); delete it.
Coordinator verification:169JVMtests pass; lint0errors/0warnings; E2EAPKs assemble; all8guards/sourcegate236pass. RealTC22 Echo native scenario completes all5stages with86/86documented semanticrequests,2accepted synthetic scans andsuccessfulcleanup. Printing remains simulated.
@@ -0,0 +1,48 @@
# Coding standards review — November 1, 2026
The scanner client received source-wide mechanical corrections and an explicit native Kotlin formatting pass. Compiler warnings now fail Kotlin compilation. The combined strict build passed with 163 JVM tests and no Kotlin warning/error diagnostics. Android lint reported zero errors, one bitmap-density warning and two informational findings; the density correction and its verification remain pending. These checks do not establish runtime acceptance or complete standards compliance.
## Scope and ownership
The review inventoried the original 2,524 tracked paths and all maintained working-tree source. The final mechanical gate covers 234 source/build/tooling files: 216 Kotlin sources, seven Gradle Kotlin files, nine JavaScript modules, one Java security probe and one Bash script. This includes shared core/mobileapp libraries, scanner host, sample application, source-set drivers, unit/native tests and CI scripts. The new About screen is included; its literal text and the Dogfooding/Query destinations are preserved by the formatter verification.
The 2,142 Material Design drawable resources retain their upstream attribution and provenance in NOTICE and the material-icons manifest. Binary art, upstream wrapper implementation, third-party license data and generated/ignored build output were excluded from ownership rewrites. Existing Kingsrook and QRunIO copyright dates/notices were preserved. New comments do not replace third-party ownership.
Static checks cover the entire maintained executable-source inventory. Named declaration-header correction covers all 216 Kotlin files and the Java probe. The declaration scanner excludes implicit property accessors, secondary constructors and anonymous objects; its counts are not a claim that every Kotlin syntax form was parsed. Manual semantic inspection concentrated on authentication/session boundaries, scanner ownership and release, capture consent/leases, transport/resource handling, widget conversion and callback registration. This is not an exhaustive semantic audit of every unchanged statement or every test assertion.
## Implemented corrections
- Added missing copyright/SPDX headers without replacing existing notices; corrected LF/final newlines and trailing whitespace.
- Replaced all 46 original wildcard imports with explicit symbols, including Compose delegation and coroutine-test extensions.
- Added useful ownership, cancellation, transport and contract comments to missing production declarations. Descriptive tests also receive the visual headers explicitly permitted by K22-CODE-09 and the QQQ code-style guidance.
- Added the [source standards gate](../scripts/ci/check-source-standards.mjs) to the existing Scanner Android verification job. It checks copyright/SPDX, UTF-8, LF/final newlines, trailing whitespace/tabs, Kotlin/Java wildcard imports and JavaScript syntax. The inventory includes tracked and nonignored untracked source, so the gate checks its own new file locally as well as in CI.
- Enabled `allWarningsAsErrors` for every `KotlinCompile` task across modules, variants and tests. No global warning suppression was added.
- Replaced unchecked widget-map casts with validated string-key copies into owned mutable state. Added regressions for immutable/shared-input ownership and malformed keys.
- Replaced the unchecked callback-function cast with checked zero-argument function registration and a return-discarding wrapper. Added regressions for malformed registration and once-per-step execution. Removed two proven redundant non-null assertions.
- Migrated the host to the already available lifecycle-compose lifecycle owner, removed a shadowed projection-listener name and retained deprecated accessibility-node recycling only on Android 26–32 with an expression-level compatibility explanation.
- Routed previously unused modifiers to the corresponding existing UI/error nodes. Five intentionally unused published renderer/dialog parameters retain their compatible signatures with parameter-level suppressions and specific explanations. These suppressions do not disable diagnostics elsewhere.
- Implemented the sample repository's previously unimplemented URI resolver with the existing production HTTP(S)/host/no-userinfo validation, and removed obsolete commented implementations. Unfinished metadata/renderer questions remain explicitly tracked in [legacy source follow-ups](LEGACY-SOURCE-FOLLOWUPS.md) and the [task list](TODO.md); deleting inline work markers does not implement those features.
## Applicable formatter and precise limitation
The existing repository sets `kotlin.code.style=official` in [gradle.properties](../gradle.properties), while [.editorconfig](../.editorconfig) requires three spaces. The shared personal style entry point and K22 standard give repository formatter configuration precedence. Kotlin therefore uses the native official structural layout with three-space indentation, rather than imposing incompatible generic Allman/control-parenthesis rules. The shared K22 no-line-length-limit rule is retained. The equivalent choice is now recorded in the [project IDE profile](../.idea/codeStyles/Project.xml) and its per-project selector.
The installed Android Studio Rabbit 2 / 2026.2.2 formatter processed isolated copies of all 223 Kotlin/KTS files. Literal-aware comparison, including nested string templates and raw literals, found identical executable tokens and literal contents for all 223. The stable output changed 202 active files. A second native dry run reported 222 files well formed and **`scanner-host/build.gradle.kts` still needing reformatting**. Its original active content was retained instead of chasing repeated formatter churn. The tool returned exit code zero even when reporting files that needed reformatting, so exit status alone is insufficient for a formatter gate.
The current GitHub workflow installs the Android SDK but does not declare an Android Studio IDE formatter installation. The existing mechanical CI gate is active; a complete native formatter CI gate requires a runner/image supplying the same existing IDE formatter and a wrapper that treats `Needs reformatting` as failure. No optional check is silently reported as a passing full formatter gate, and no alternate formatter framework was introduced.
Relevant shared requirements: K22-CODE-01/02 (gates/warnings), 03–09 (layout and headers), 11/12 (inactive code and tracked work), 14 (imports), 20–22 (errors/private data/input validation), 24/25 (license headers/generated exclusions) and 26 (published API documentation). The remaining formatter file/CI provisioning and parser/semantic-review boundaries prevent claiming every standard is fully proven.
## Verification for the current cohort
- Source standards gate: **234 files passed**, including the new About screen and gate itself.
- JavaScript syntax: all nine maintained modules passed without executing their device/server operations.
- Native formatter comparison: **223/223 executable-token and literal checks passed**; 222/223 dry-run reports well formed, with the exact retained script exception above.
- Combined strict build: **passed** in 1 minute 11 seconds. Actual JVM reports contain **39 core + 53 mobileapp + 71 scanner-host = 163 tests**, with zero failures or skips. All four new warning-fix regressions are included. No Kotlin `w:`/`e:` diagnostics were emitted under warnings-as-errors.
- Scanner debug Android lint: **zero errors, one warning, two informational findings**. `IconLocation` identifies the new `query_mascot.png` in `res/drawable`; moving the unchanged image to `drawable-nodpi` and rerunning lint/debug assembly are pending. The informational findings recommend specialized integer state holders.
- Existing environment/toolchain advisories remain: SDK XML version mismatch, Java 21 warnings about source/target 8 configuration, and deprecated Gradle features. They are distinct from Kotlin source warnings and are not represented as a wholly warning-free build.
- QA/native-device verification is a separate coordinating run and remains pending in this report. JVM/build success must not be substituted for its result.
No provider, physical scanner, projection, GPS or release acceptance gate is closed by this standards work. Those results remain in their existing readiness/acceptance records.
Final follow-up: the unchanged Query bitmap now lives in drawable-nodpi; debug APK rebuild and lint pass with zero errors and zero warnings (two informational findings remain). All46 sample native tests pass on TC22 with no failures/errors/skips, and QA instrumentation compiles. Toolchain advisories and the formatter CI limitation above remain explicit.
+85
View File
@@ -0,0 +1,85 @@
# Company scanner profile package
Status 2026-11-02: **prepared, not applied.** Nothing here has been installed on, pushed to or tested against a device. No device, setting, credential or server was changed. The target device's Android build, MX version and installed EHS version have **not been measured**; Zebra documentation versions below are the documentation reviewed, not device facts. Context and layering: `docs/DEVICE-PROVISIONING.md`.
Files: `deployment/company-profile/`
| File | Purpose |
| --- | --- |
| `profile.example.json` | Non-secret parameters. Unmeasured or unknown values are `null` or placeholders on purpose. |
| `generate-company-profile.mjs` | Validates the profile and renders `enterprisehomescreen.xml`, `managed-configuration.json` (once the endpoint is real) and `stagenow-mx-appmgr.xml` (only when the device is measured). No dependencies. |
| `generate-company-profile.test.mjs` | `node:test` cases for rendering, withholding and rejection. |
| `DEVICE-OWNER-ENROLLMENT.md` | The separate, destructive enrollment path. Not generated. |
```text
node deployment/company-profile/generate-company-profile.mjs my-profile.json out/ [--require-ready]
node --test deployment/company-profile/
```
Exit 1 on validation errors; 2 with `--require-ready` while any input is still missing. `out/profile-report.json` lists `missing` and `warnings`. One shared APK serves every customer: customer differences are only profile values (title, logo, wallpaper, app list), never a build.
## What the generated EHS file contains
Only tags from Zebra's EHS 8.0 Advanced Settings (`techdocs.zebra.com/ehs/latest/guide/settings/`): `kiosk`, `applications`, `tools`, `passwords`, and in `preferences` the `title` (static, or `<value>` with one documented device-ID token), `title_bar_icon` (`title_bar_icon_disabled`, `title_bar_icon_file`, `reuse_custom_icon`), icon label colours, `orientation`, `wallpaper`, `wallpaper_stretching_enabled`, the keyguard/USB/settings/recent-apps tags, `wireless_info` and `screen_blanking`. Tags not parameterised keep the values of Zebra's documented default file. Constraints enforced from that page: images only under `/enterprise/usr` (the persistent location), documented image formats, app labels of at most 18 characters, XML escaping, no wildcard packages (rejected for least privilege), no EHS self-listing.
Layout: EHS shows apps in listed order (links first when both exist). Arbitrary pixel placement is not a documented feature; folders are not in the pages reviewed and are not generated.
Explicit non-goals, enforced by the generator: `usb_debugging_disabled` is always `1`, `kiosk_mode_enabled` is always `0`, no `bypass keyguard`, `<passwords>` is always empty, `launcher.iconSize` is rejected (the Zebra snippet does not show where `user_options` sits).
## What the generated MX file contains
`AppMgr` characteristics only (`techdocs.zebra.com/mx/appmgr/`): optional `Install`/`Upgrade` of an APK already on the device, then `SetDefaultLauncher` for `com.zebra.mdna.enterprisehomescreen` (EHS setup page, `.../ehs/latest/guide/setup/`). Characteristic version `4.2` is the version used in Zebra's examples; `SetDefaultLauncher` is listed as MX 4.1+. **Not generated:** AccessMgr (allowlist and permission grants can lock out settings or break the scanner; Zebra's schema needs a device-specific review) and file delivery of the EHS XML and images to `/enterprise/usr` (Zebra recommends StageNow, DNA Cloud or an EMM for moving files; the StageNow file-delivery parameters were not in the cached pages, so they are not guessed).
## Hazards to review before any device
1. **EHS defaults disable USB debugging.** Zebra: `usb_debugging_disabled` 1 "or if left unspecified will prevent user access to the file system and all ADB functionality". Installing EHS on the current development TC22 will cut ADB. Use a spare device; Zebra advises restoring USB debugging before uninstalling EHS.
2. **Pushing the XML overwrites the existing file**, including the stored admin password hash. Set the EHS admin password in Admin Mode on a reference device; never author it. Decide whether to deploy that device's exported file.
3. **`system_settings_restricted` 1 (default) hides most Settings panels** and Zebra warns it might prevent some MX features. Confirm Bluetooth printer pairing and Wi-Fi workflows. The profile requires this value explicitly.
4. **Default launcher:** EHS must be re-designated after any reinstall. Test Home/Back/recovery before relying on it.
5. **MediaProjection consent is still per session** on Android 14; nothing here grants it.
6. **Managed bootstrap is consumed by the app, but only through an EMM.** `managed-configuration.json` is a reviewable key/value payload for the EMM's Android managed-configuration (app restrictions) field; StageNow/MX and the EHS XML do not deliver it, and no Zebra schema is claimed for it. See "Managed configuration" below.
## Inputs still required (also in `profile-report.json`)
- Measured on the target TC22: `device.androidVersion`, `device.mxVersion`, `device.osBuild`; choose and record `ehs.installedVersion` for that firmware (docs reviewed: EHS 8.0).
- Company logo and wallpaper image files, delivered to `/enterprise/usr/...`, and their paths.
- Real company `qqq.endpoint` (HTTPS origin) and optional `qqq.orientation`; the EMM to push them as app restrictions.
- Administrator list of permitted Android companion apps (camera/photo picker, printing) with exact package and activity names from the device.
- On-device EHS APK path and the artifact delivery/checksum method; the QQQ APK path if the profile should also deploy it.
- Decisions: `systemSettingsRestricted`, recent-apps values, `show IP` visibility, who sets the EHS admin password, EMM versus StageNow, and whether a device-owner fleet is wanted.
## Managed configuration (implemented in the shared APK)
The app declares `res/xml/app_restrictions.xml` (manifest `android.content.APP_RESTRICTIONS`) and reads it with `RestrictionsManager` (`ManagedConfiguration.kt`). Two keys, both optional; unmanaged installs behave exactly as before.
| Key | Values | Behaviour |
| --- | --- | --- |
| `qqq_server_url` | HTTPS origin only: no credentials, path, query or fragment; HTTP is rejected even in debug builds | No saved server: the connect form is prefilled with it and says the administrator configured it; the person still taps Connect. Different saved server: never switched silently. Settings shows a notice with "Use administrator's server", which runs the existing confirmed Change-server path (ends the session, logs out) and then shows the prefilled form. |
| `qqq_orientation` | exactly `SYSTEM`, `PORTRAIT`, `LANDSCAPE`, `REVERSE_PORTRAIT`, `REVERSE_LANDSCAPE` | A bootstrap default applied when the managed value changes (last applied value is remembered); a later local choice stands until the administrator changes it again. Unknown values are rejected, not defaulted. |
Changes are re-read when the host resumes and while it is resumed (system `ACTION_APPLICATION_RESTRICTIONS_CHANGED`, not exported). Rejected keys are tracked by name only; values are never logged. No credentials, business metadata or branding are accepted this way. In the profile: `qqq.endpoint` and optional `qqq.orientation` produce `managed-configuration.json` (withheld while the endpoint is a placeholder). The EMM must be told to push those two keys as app restrictions for `io.qrun.mobile.scanner`.
## Dynamic status wallpaper (implemented, optional)
EHS has no QQQ-user or IP field (title tokens are device IDs only; Wireless Info shows the IP when `show_ip_address` is 1, exposed as `launcher.showIpAddress`), so the shared APK carries an optional native live wallpaper: `QqqStatusWallpaperService`, declared with the system-only `BIND_WALLPAPER` guard. It is inactive until a person picks it: Settings → "Status wallpaper…" opens Android's standard live-wallpaper chooser preselected to it. Nothing is applied silently; no new permission and no second app.
- **What it shows:** company brand name always (the server's public brand name/colour already used by the host, or the default). On the unlocked home screen only, also: signed-in display name, server host and the default network's IP (address and interface, via a network callback, no polling). Preview, lock screen, a locked keyguard, a missing keyguard service and lock-only wallpaper show the brand alone. Identity is withdrawn when the screen turns off or the wallpaper stops being visible.
- **Freshness is not overstated.** The existing heartbeat runs only while the app is foreground. Shown while foreground: "Signed in: NAME". After the app leaves the screen: the name with "Last confirmed HH:mm (app in background)". After 15 minutes in the background the name is replaced by "Session unverified". States are UNKNOWN (new process, destroyed host), SIGNED_OUT and SIGNED_IN; the snapshot lives only in memory, so process death and server change return it to UNKNOWN, and logout, revocation and unauthorized responses clear the name. It holds no token, cookie, credential or session id and logs nothing.
- **Limit:** a wallpaper is visible only where the launcher or a transparent window shows it. Whether EHS reveals it is unproven; EHS draws its own `<wallpaper>` image, so leave that empty when testing.
## Status of the implementation
| Item | Status |
| --- | --- |
| Wallpaper service, manifest/resources, managed configuration, settings entry, activity wiring | Compiled into the debug and E2E APKs built after the last source edit (2026-11-02 15:34). Not installed; not run on a device. |
| JVM unit tests `WallpaperStatusTest` (13), `ManagedConfigurationTest` (8) | Pass in the host unit-test run after the last source edit. |
| Generator tests for managed keys, URL and orientation | Seven generator tests pass (root run). |
| Visibility behind EHS, lock-screen behaviour, keyguard/wake timing, Android 14 wallpaper flags on a TC22, EMM delivery of restrictions | **Untested on hardware.** |
APIs used that should be confirmed by the build and a device, because the SDK jar was not readable in this session: `WallpaperService.Engine.getWallpaperFlags()` (API 34, guarded), `KeyguardManager.isKeyguardLocked`, `ConnectivityManager.registerDefaultNetworkCallback` (API 24), `RestrictionsManager.getApplicationRestrictions`, `WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER` / `EXTRA_LIVE_WALLPAPER_COMPONENT`, `ContextCompat.registerReceiver` with `RECEIVER_NOT_EXPORTED` (core 1.13.1).
## Validation status
- Authored against the cached Zebra pages `qqq-zebra-docs-ehs-settings.md`, `-ehs-setup.md`, `-mx-appmgr.md` and `-mx-accessmgr.md` (untrusted reference content; facts above cite their official page).
- Seven generator tests pass (`node --test deployment/company-profile/`), including managed keys, URL and orientation.
- Nothing has been validated against Zebra's StageNow importer or a device.
+32
View File
@@ -0,0 +1,32 @@
# Stable authentication and image dependencies
Reviewed October 31, 2026. This decision replaces Auth0 Android `3.0.0-beta.0` with `3.0.0` and all three Coil `3.0.0-rc01` version aliases with `3.0.4`. It preserves the current Kotlin, Compose compiler, AGP, Java and Android SDK configuration. These are bounded stable replacements, not a claim that every dependency is the latest release or has ongoing maintenance guaranteed by its publisher.
## Auth0 Android
Auth0 `3.0.0` is the last stable 3.x release before passkey dependencies were introduced in `3.1.0`. An Auth0 maintainer explicitly recommends it for applications avoiding the added Play Services dependency in [upstream issue 871](https://github.com/auth0/Auth0.Android/issues/871). Its [published POM](https://repo.maven.apache.org/maven2/com/auth0/android/auth0/3.0.0/auth0-3.0.0.pom) declares the same direct dependency coordinates and versions as the current beta. It retains Kotlin stdlib 1.8.22, OkHttp 4.12.0 and Gson 2.8.9. Its source targets Java 8 and supports Android API 21; this application uses API 26 or later.
The current host uses `Auth0.getInstance`, `WebAuthProvider.login/logout`, audience/scheme configuration, and browser callbacks. Native semantic OAuth login uses AppAuth separately. The application does not use Auth0's credential managers, passkey provider, DPoP, PAR or native-to-web credentials.
The stable release adds AuthenticationActivity null handling and changes the fallback for an empty authentication error description. Later 3.x changes include browser process-death restoration in 3.4, configurable authorization/logout URLs, optional DPoP and its replay/process-restoration fixes, partial browser tabs and PAR. Later RSA-padding, biometric and session-expiry fixes concern Auth0 credential managers that this client does not use. See the [tagged changelog](https://github.com/auth0/Auth0.Android/blob/3.21.3/CHANGELOG.md) and [3.0.0 to 3.21.3 source comparison](https://github.com/auth0/Auth0.Android/compare/3.0.0...3.21.3).
The bounded comparison found no later security fix applicable to the current WebAuth usage; the public repository advisory endpoint returned no published advisories at review time. Neither observation is a complete security audit or a maintenance guarantee. Full process-death recovery through Auth0's later application callback APIs is not provided by choosing 3.0.0. Existing late-callback, rotation, logout and origin-isolation protections still require regression testing.
Auth0 3.21.3 is a stable alternative, but its [POM](https://repo.maven.apache.org/maven2/com/auth0/android/auth0/3.21.3/auth0-3.21.3.pom) adds Credential Manager and its Play Services provider. The [provider POM](https://dl.google.com/dl/android/maven2/androidx/credentials/credentials-play-services-auth/1.3.0/credentials-play-services-auth-1.3.0.pom) introduces Google authentication, FIDO and Google ID artifacts. Upstream publishes them as regular dependencies; no supported exclusion recipe was found. This change does not exclude required classes or waive notice checks. Version 3.9.0 is not a dependency-free alternative: its missing transitive declarations were a publication defect repaired in 3.9.1.
## Coil
Coil 3.0.4 is a stable patch in the existing major/minor line. Its [release notes](https://coil-kt.github.io/coil/changelog/#304---november-25-2024) include image rendering and cache corrections. The intervening RC removed the `AsyncImage` equality-delegate parameter, which this repository does not use. Both image consumers use supported `ImageLoader`, `SvgDecoder.Factory`, `ImageRequest` and `AsyncImage` APIs.
Published POM comparison predicts Kotlin runtime 2.0.20 to 2.0.21, lifecycle runtime 2.8.6 to 2.8.7 and annotations 1.8.2 to 1.9.1. Compose foundation 1.7.0, coroutines 1.9.0, OkHttp 4.12.0, Okio 3.9.1 and AndroidSVG 1.4 remain unchanged. These are requested dependency versions, not a substitute for inspecting Gradle's final selected graph. See [Compose](https://repo.maven.apache.org/maven2/io/coil-kt/coil3/coil-compose-core-android/3.0.4/coil-compose-core-android-3.0.4.pom), [core](https://repo.maven.apache.org/maven2/io/coil-kt/coil3/coil-core-android/3.0.4/coil-core-android-3.0.4.pom), and [SVG](https://repo.maven.apache.org/maven2/io/coil-kt/coil3/coil-svg-android/3.0.4/coil-svg-android-3.0.4.pom) metadata. No toolchain upgrade or forced transitive downgrade is included.
## Qualification required after these edits
- Compare debug, release, E2E and QA runtime graphs; verify the stable versions, aligned Coil modules, Kotlin/Compose selections, absence of newly introduced Google Play dependencies and continued compile-only EMDK isolation.
- Run core/mobile/scanner JVM suites, lint and scanner/sample APK and instrumentation compilation. Inspect release manifests and bundled native-library hashes again if their resolved dependencies change.
- Regenerate and validate each variant's complete AboutLibraries report and packaged texts. New graph size must be measured; preserve exact coverage and license gates.
- Repeat native authentication/navigation regressions, including legacy Auth0 callbacks and provider-failure logout. Configured-provider browser acceptance remains a separate deployment check.
- Verify raster and SVG branding/widget rendering, loading/error states, image-loader disposal, and offline license viewing. Existing upload and screen-mirror tests alone do not prove Coil image rendering.
- Requalify the TC22 scanner flow on the resulting APK before delivery. Do not treat earlier APK evidence as validation of these new pins.
The candidate now passes127 JVM tests, all34 selected TC22 shared native cases (including actual PNG/SVG/error rendering), the scanner overlay regression, package/lint gates and four-APK inspection. Actual packaged reports contain131 entries/nine complete texts and the expected stable Auth0/Coil versions with no new Google identity dependencies. Selected Kotlin stdlib is2.0.21 and reflect2.0.20; historical JDK7/8 compatibility wrappers remain1.9.0. These observed selections are not a claim of a broader toolchain upgrade. See [exact package evidence and remaining gates](TRUSTED-SCANNER-ACCEPTANCE.md). No production release has been signed or published.
+89
View File
@@ -0,0 +1,89 @@
# Company provisioning for Zebra handhelds
Research checked 2026-10-31. Target: Zebra TC22, Android 14, DataWedge 13.0.325, QQQ Mobile package `io.qrun.mobile.scanner`. This is a design recommendation, not an installed profile or a completed hardware qualification. No device configuration, enrollment, reset or runtime changes were made for this research. Production signing and distribution remain deferred.
## Recommended approach
Use Zebra StageNow/MX for repeatable initial setup, Enterprise Home Screen (EHS) for the company launcher, and the customer's existing Android Enterprise EMM/MDM with Zebra OEMConfig for ongoing fleet management. Keep QQQ responsible for its own device approval, users, sessions, business apps and semantic process UI. A QQQ-approved device and an Android-managed device are separate states.
Start with a static company-branded EHS home screen and QQQ's own live status view. Showing the QQQ user and IP continuously on the Android background is feasible as additional development, but is not a documented EHS configuration-only feature. Validate a small proof of concept before making dynamic wallpaper part of the release requirement.
| Layer | Handles | Important boundary |
| --- | --- | --- |
| StageNow and MX | Initial network/certificate setup, app/file deployment, supported OS settings and permission controls | A staging profile is not a persistent fleet-management service. Features depend on installed MX, Android and device support. |
| EMM plus current Zebra OEMConfig | Ongoing policy, application distribution and Zebra-specific managed configuration | Requires a management system and appropriate enrollment. Its licensing is separate from QQQ. |
| Enterprise Home Screen | Allowed Android apps, labels, icons, ordering/folders, launcher branding, launcher restrictions and optional kiosk mode | Does not alone provide complete device lockdown. Its orientation setting controls EHS screens only. |
| QQQ Mobile | Customer branding inside the shared APK, sign-in, device approval, sessions, mobile apps and semantic actions | An ordinary app cannot silently administer every Android setting or permission. |
These are supported product roles, not a proposal to build an MDM into QQQ. [StageNow overview](https://techdocs.zebra.com/stagenow/latest/about/), [OEMConfig overview](https://techdocs.zebra.com/oemconfig/latest/about/), [EHS overview](https://techdocs.zebra.com/ehs/latest/guide/about/).
## Versions, provisioning and privileges
The documentation reviewed identifies StageNow 5.22, OEMConfig 15.3 and EHS 8.0. EHS added explicit TC22/TC27 Android 14 support in 7.0. Select a supported EHS build for the actual TC22 firmware; a current documentation version does not establish the installed MX version. Check the device's Android build, MX version and relevant feature matrix before generating executable profiles. [EHS release history](https://techdocs.zebra.com/ehs/latest/guide/about/), [StageNow release history](https://techdocs.zebra.com/stagenow/latest/about/), [MX documentation](https://techdocs.zebra.com/mx/).
StageNow creates XML-based profiles on Windows and packages staging instructions as barcodes or binary files for its device client. Configuration can be staged offline, but referenced APKs, certificates and images must already be available or downloadable. Do not describe a barcode that points to a network-hosted APK as a completely offline installation. StageNow's workstation tool includes an FTP server; use an appropriate protected artifact-delivery method in production rather than assuming the workstation server is a production distribution service. StageNow Dynamic Staging substitutes deployment values when generating staging artifacts; it does not create a live wallpaper connected to QQQ sessions. [Getting started](https://techdocs.zebra.com/stagenow/latest/gettingstarted/), [Staging profiles](https://techdocs.zebra.com/stagenow/latest/stagingprofiles/), [Dynamic Staging](https://techdocs.zebra.com/stagenow/latest/dynamicstaging/).
For Android 14 use **Zebra OEMConfig Powered by MX**, package `com.zebra.oemconfig.release`. The legacy `com.zebra.oemconfig.common` package is not compatible with Android 13 and later. OEMConfig exposes Zebra's managed-configuration schema to an EMM; it does not replace the EMM. Verify support for each selected MX setting on the installed firmware. [OEMConfig requirements and compatibility](https://techdocs.zebra.com/oemconfig/latest/about/).
Applying supported Zebra configuration or installing EHS is distinct from Android Enterprise enrollment. Fully managed device-owner provisioning is intended for initial setup or a factory-reset device; a work profile controls a different scope and is not equivalent to owning the entire handheld. Decide the management approach before a production enrollment exercise. Do not reset the current development TC22 merely to try EHS. [Android device management](https://source.android.com/docs/devices/admin).
MX AppMgr can install/manage apps and select the default launcher. MX AccessMgr can control supported permissions, settings access and application allowlists. Permission Access Action requires MX 10.0.5.1 or later, and individual capabilities have their own prerequisites. App removal or reset can invalidate grants. Grant only capabilities the installed app declares and needs; do not interpret these controls as a blanket bypass of Android security. In particular, Android MediaProjection screen capture has a per-session user-consent requirement for apps targeting Android 14+, separate from ordinary runtime-permission grants. [AppMgr](https://techdocs.zebra.com/mx/appmgr/), [AccessMgr](https://techdocs.zebra.com/mx/accessmgr/), [Android 14 capture consent](https://developer.android.com/about/versions/14/behavior-changes-14#media-projection-consent).
EHS is described by Zebra as free. The TC22 is a Professional-series device; some additional DNA capabilities require separate licensing. Zebra specifically identifies advanced DataWedge multibarcode and image/document-capture capabilities as requiring a Mobility DNA Enterprise license on Professional devices. That does not establish a license requirement for ordinary Android camera capture used by QQQ. Confirm the selected feature entitlement and EMM costs with the supplier; no license pricing or blanket entitlement is assumed here. [EHS overview](https://techdocs.zebra.com/ehs/latest/guide/about/), [Zebra licensing](https://techdocs.zebra.com/licensing/about/).
## Company home screen and navigation
EHS configuration can define permitted apps/shortcuts and their labels/icons, title/logo, wallpaper and orientation. Apps follow configured ordering, with a documented exception that links appear before apps when both are present. This provides an ordered managed launcher, not a promise of arbitrary pixel-positioned icons. Current EHS also documents folder support. [EHS settings](https://techdocs.zebra.com/ehs/latest/guide/settings/), [EHS release history](https://techdocs.zebra.com/ehs/latest/guide/about/).
Use multi-app EHS initially: Android Home returns to the company launcher; QQQ's own home/menu navigates among QQQ business apps. Allow only the required Android companion apps, including any camera/photo-picker activities needed by the chosen capture flow. A single-app kiosk deliberately changes Home/Back escape behavior and needs a separate recovery and navigation test. EHS's portrait/landscape setting affects its own screens; the scanner's orientation preference remains an app setting. [EHS kiosk and orientation settings](https://techdocs.zebra.com/ehs/latest/guide/settings/).
EHS supports static wallpaper files in common image formats, preferably under `/enterprise/usr`; Zebra says `/enterprise` content survives an Enterprise Reset. This is not a guarantee of factory-reset persistence. Use supported StageNow/EMM deployment and appropriate access controls. Do not introduce world-writable configuration files as a convenience. Protect EHS administrator access and test the recovery procedure before enabling launcher restrictions. [EHS configuration and storage](https://techdocs.zebra.com/ehs/latest/guide/settings/), [EHS setup](https://techdocs.zebra.com/ehs/latest/guide/setup/).
## Live user and IP information
EHS provides two relevant built-in facilities:
- Its title can contain a device identifier such as Android device name, serial number, Wi-Fi host name or Bluetooth name. The documented substitutions do **not** include a QQQ user or IP address.
- Its Wireless Information screen can expose the currently assigned IP to users using `show_ip_address`. That is a diagnostic screen, not a documented live wallpaper field. Other network details can remain hidden.
Both capabilities are documented in [EHS settings](https://techdocs.zebra.com/ehs/latest/guide/settings/). StageNow generation-time substitutions do not fill this runtime gap.
| Option | Assessment |
| --- | --- |
| Static company wallpaper plus QQQ live status | Recommended first step. Reliable branding; current user, connection and IP can be presented by the app that owns the session. |
| QQQ status/dashboard activity reachable from EHS | Small additional surface if status must be easy to inspect outside a business process. It can be added to the EHS app list after implementation. |
| Event-updated wallpaper bitmap | Technically possible using Android WallpaperManager when policy permits. Must verify visibility under EHS and update/clearing behavior; periodically rewriting the EHS XML is not a recommended session transport. |
| Android live wallpaper | Requires a WallpaperService/Engine component and activation. EHS compatibility, policy, background lifecycle and resource use require a TC22 proof of concept. Not a documented EHS native feature. |
| Home-screen widget/custom launcher | A widget needs a launcher that hosts it. EHS third-party widget hosting was not established by the documentation reviewed; do not promise it. A custom launcher is a substantially larger maintenance commitment. |
Android exposes wallpaper support/permission checks and bitmap setters; live wallpaper is a separate service contract. These APIs establish feasibility, not proof that EHS renders a chosen live wallpaper or that management policy allows it. [WallpaperManager](https://developer.android.com/reference/android/app/WallpaperManager), [WallpaperService](https://developer.android.com/reference/android/service/wallpaper/WallpaperService).
The authoritative identity must come from QQQ's current session. EHS administrator mode, Android device owner and QQQ sign-in are different identities. Publish only an approved display name and state; never publish session tokens or credentials. Clear the name on logout, expiry, remote termination and server/customer change. Show an explicit stale/offline state if session validity cannot be confirmed. Avoid personal identity on the lock screen by default; any generated session image should be excluded from backup and replaced when its owner changes.
For IP updates, use Android network callbacks and LinkProperties instead of polling or a single cached Wi-Fi address. Handle network loss, DHCP changes, IPv4/IPv6, VPNs and multiple transports; label the displayed interface/address. A local IP alone does not prove that the QQQ server is reachable. [Android network state and callbacks](https://developer.android.com/develop/connectivity/network-ops/reading-network-state).
## Proposed profile parameter inventory
These are design inputs, not a finalized XML schema or implemented managed-configuration contract.
| Group | Non-secret parameters |
| --- | --- |
| Profile identity | Customer/profile ID, revision, target model/Android/MX range, rollout ring, rollback revision |
| Device identity | Assigned short name, description, asset ID; explicitly map Android device name and QQQ device record |
| Network/trust | Network-policy reference, certificate/credential delivery reference, proxy/DNS/time policy; no embedded passwords in repository fixtures |
| Apps | Package IDs, approved version/channel, artifact reference/checksum, signing identity once decided, required companion activities |
| Launcher | Title, logo/wallpaper asset references, ordered app list/folders, multi-app or kiosk policy, admin recovery reference |
| Scanner | DataWedge profile association, expected intent/output contract, required decoders; preserve the verified QQQ scan transport |
| App bootstrap | HTTPS QQQ server URL, tenant/branding discovery hints, supported configuration revision, optional orientation policy |
| Capabilities | Camera/location policy, diagnostics visibility, screen-sharing policy, required permission decisions; no blanket grants |
| Operations | Update window, reporting/retention policy, support contact, rollback and recovery procedure references |
QQQ can support Android managed app configurations without customer-specific builds, but the app must declare and consume the configuration schema. Source inspection found no RestrictionsManager/app-restrictions declaration or wallpaper component in the current scanner host. This is proposed work. Managed settings should supply bootstrap/policy only; business screens and processes remain on the versioned QQQ semantic API. Reconcile admin-managed values with user preferences explicitly. [Android managed configurations](https://developer.android.com/work/managed-configurations).
## Safe qualification and rollout
1. Inventory firmware/MX/StageNow/EHS versions and selected feature entitlements. Choose existing customer EMM or staged-only pilot. Preserve current scanner configuration and a tested administrator recovery route.
2. Produce a versioned, customer-parameterized profile with no repository secrets. Review settings and exclusions before generating installable artifacts. Keep development debugging available on a development-only profile.
3. Test on a spare TC22 first: network/trust, installation, app approval, scan delivery, camera uploads, location behavior, Home/Back/menu/logout and orientation. Check reboot and network-change behavior, plus management connectivity loss.
4. Verify restrictions and recovery together: app allowlist, settings access, permission denial, policy update, rollback and retained data. Test the dynamic status option separately, including logout/remote revocation, stale state and lock-screen visibility.
5. After signing/distribution decisions, qualify signed upgrades and management deployment in a small pilot ring. Expand only after documented acceptance. Device-owner enrollment/reset and production kiosk activation remain explicit rollout actions, not part of this research.
+24
View File
@@ -0,0 +1,24 @@
# Device-screen viewing acceptance
Current status: this page retains its dated/source-specific checkpoint. Current installed C891/earlierF3AF EMDK Code128 proof is in [physical acceptance](PHYSICAL-TC22-ACCEPTANCE.md); QR and actual projection remain pending. Android7de passes all eleven hosted checks. Older statements about then-pending trigger/CI/scope or unchanged normal APK are historical, not the current release gate. Normal server remains mobile1.1; optional DEVICE_SCREEN requires qualified1.2 source and actual device consent acceptance.
The optional `DEVICE_SCREEN` mode requires the additive mobile-scanners1.2 contract. `APP_CONTENT` remains the default for older servers. The device operator must explicitly enable notifications, accept the disclosure and approve Android's fresh screen-sharing dialog. No consent is persisted. The foreground notification and Android indicator allow stopping; sharing also stops on host disposal, lock, session loss or bounded lease/watchdog expiry. Remote revocation is learned through subsequent requests, rather than an instantaneous push.
The initial source checkpoint passes135 JVM tests with no failures/errors/skips, scanner debug/release lint, and debug/unsigned-release/E2E/QA plus native-test package builds. Its paired server passes39 tests and six real-HTTP demo tests using qualified core `ecf0f550`, with no artifact installation and unchanged historical1.0/1.1 API baselines. The frozen client diff received parent and fresh independent static review: all nine inventoried changed files inspected, no reportable findings. The sealed security scan is `ea8a20a8-8a16-48d8-9c58-a9376387fb60`; its tool reports4,884,517 aggregate tokens across two threads. This is a changed-file static review, not a new full-repository audit or device acceptance.
The refined candidate passes137 JVM tests, debug/release lint and all package builds. A separate independent review covered its four-file image-buffer/binding follow-up, with no concrete issue found; the original sealed scan remains unchanged and describes only its original bytes. The TC22 ordinary-UID/distinct-signer regression passes on the refined QA APK: eight forged broadcasts add zero mutations while one manual positive control saves. Physical trigger and projection consent remain unverified.
All four refined APKs pass fresh SDK DEX/manifest/resource inspection:131 notice entries/nine full license texts, unchanged reviewed native binaries, zero bundled EMDK implementation classes, test-only ingress excluded from production variants, backup exclusions intact, and unsigned release cleartext/debugging disabled. The paired published server `10416938724c9663d66f7848ff2b1a63fe635794` also passes a fresh isolated consumer qualification:195 hashed runtime JARs,2,441 unique QQQ classes, zero duplicates,39 server/six demo tests and all15 record/bulk/admin browser tests, with no shared Maven replacement. Hosted client CI is pinned to that exact source.
The refined candidate hashes are:
| APK | SHA-256 |
| --- | --- |
| Debug | `F3AF638E34AA0B6FB44D789362BED2838202EDAF681DF775F2A5FAD3AB3E9FAA` |
| Unsigned release | `69FF9A2F1D7A0A766682994E393D1DE9E3B10CA28D1ECD7239D43E7FB8C57B8C` |
| QA | `8F44EFB9A025F0E9457057E25844852BC83B0823B87B5D830AB93BD7962B80EB` |
| E2E | `CCB447F8F4121E0FBCF6B8DD5A82784DACCBAF2AF839A10127B6E4CE517CDFBF` |
Actual OS consent, notification visibility, fresh pixels on a static screen, other-app/background capture, lock, rotation/recreation, administrator disconnect and forced logout remain required device acceptance. A projection may exist locally for up to30seconds awaiting a viewer; uploads require a current matching-mode lease. Binding stays disabled through service startup; continuous draining retains only one latest image, released on replacement/resize/stop. These APKs are not a production release.
November1 operator update: the debug APK above was installed in place into the normal TC22 app after matching its signing identity and backing up private data. Login and Apps/Scan Capture navigation succeeded; EMDK reports ready. Normal server remains API1.9/mobile1.1 and does not advertise whole-device mode. This update does not establish projection acceptance or qualify historical DataWedge scans as EMDK scans.
+49
View File
@@ -0,0 +1,49 @@
# Isolated TC22 acceptance
The emulator-only CI entry point continues to refuse physical devices. The explicit physical entry point is limited to an attached Zebra TC22 and requires its serial and one named scenario:
```powershell
$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk"
node scripts/ci/scanner-device-e2e.mjs --serial TC22_SERIAL --scenario session-monitor ../qbit-mobile-scanners
node scripts/ci/scanner-device-e2e.mjs --serial TC22_SERIAL --scenario navigation
node scripts/ci/scanner-device-e2e.mjs --serial TC22_SERIAL --scenario licenses
node scripts/ci/scanner-device-e2e.mjs --serial TC22_SERIAL --scenario orientation
```
Build the matching APKs first, using Java 21 and one bounded Gradle invocation (`--max-workers=2 --no-parallel`). Scanner scenarios need `:scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest` and the companion demo's installed artifact/classes/test classpath. The navigation scenario needs `:qqqsampleandroidmobileapp:assembleDebug :qqqsampleandroidmobileapp:assembleDebugAndroidTest`.
The handheld must already be unlocked. This runner does not change power, lock, animation, developer, DataWedge or ordinary scanner settings. It refuses existing disposable test packages, installs only separate test applications, and removes only applications installed by that run. The ordinary `io.qrun.mobile.scanner` installation and its data are never cleared or replaced. Scanner scenarios use a temporary local server/database and one new non-rebinding USB reverse, which is removed on cleanup. Existing reverses, including the operator's ordinary server connection, remain in place.
`session-monitor` exercises real login/enrollment/approval, process activity, recreation, actual PixelCopy upload, background/paused-frame clearing, administrator session termination and fresh login. `navigation` runs the shared native navigation, input and account suites with synthetic repository data and verifies request counts/draft preservation. It installs the sample app temporarily and refuses to replace any existing sample installation or retained app data. `licenses` opens the bundled notice list and renders the Google artwork license; `orientation` checks the app's orientation policy. These two scenarios use only the isolated scanner APK/test APK and need no demo server or USB reverse. Other scanner scenario names are the same as [EMULATOR.md](EMULATOR.md).
Evidence is written under `scanner-host/build/native-device-e2e`. Request audits contain operation IDs/status/timing, not credentials or bodies. Do not capture the normal app's screens or customer data as test evidence. Automatic camera transport tests must use synthetic originals; a real camera shot requires the operator to aim at an agreed non-sensitive target. The current implementation and test outcomes are recorded separately in [SESSION-STATE.md](SESSION-STATE.md).
The standalone `orientation` and `licenses` scenarios need only the isolated scanner APKs and no server. Orientation acceptance checks saved policy, actual portrait/landscape configuration, recreation, and a fresh Activity launch for each choice. The session and photo scenarios separately verify that a rotation retains process input and a pending original.
Physical `session-monitor` now uses the disposable `io.qrun.mobile.scanner.qa` variant so a retained `.e2e` installation is preserved. Build it with `-PscannerTestBuildType=qa :scanner-host:assembleQa :scanner-host:assembleQaAndroidTest`; hosted/emulator sessions continue using E2E. Its rotation check waits for the requested landscape configuration and resumed lifecycle, verifies the draft, then explicitly recreates and verifies the draft again. Brief physical-test focus transitions wait up to five seconds within the existing stage deadline; actual resumed/unlocked/focused requirements remain enforced. Physical installs use ADB's non-streaming path.
## Explicit operator-consented device-screen acceptance
This manual-only test is excluded from hosted/default scenarios. Compile QA packages as above, then run:
```text
node scripts/ci/scanner-screen-device.mjs --serial TC22_SERIAL --fixture PRIVATE_FIXTURE.json --operator-consent
```
The private file must identify `purpose: "device-screen-qa"`, a disposable loopback `baseUrl`, `username: "scanner"`, `adminUsername: "admin"` and fresh private `password`/`adminPassword` values. Use the qualified core/API1.9 and mobile-scanners1.2 runtime; the runner refuses the normal57832 and retained51631 ports. Keep this file owner-only and outside Git. It installs only absent QA/QA.test packages and owns only its new reverse/proxy resources.
Named stages request notification permission, three fresh Android projection approvals, Home, Stop and a return through Recents. The operator handles every Android permission dialog; the test never grants them. It checks real app/Home pixels, Stop, viewer-lease expiry, administrator session end and fresh login without resumed projection. Each operator wait is bounded to120seconds. Server JPEGs are compared with the physical display in memory using bounded spatial RGB error and blank/wrong-layout negative controls. Neither screenshot source is retained as evidence or substituted for the application's uploaded frame.
Compilation and runner guard checks pass; actual system consent/display acceptance remains unexecuted. A failed run records FAILED rather than leaving a successful-looking proof, preserves sanitized stage/operation evidence and removes only its disposable resources. Successful execution requires every completion stage and successful native semantic screen uploads; a decodable JPEG alone is insufficient.
Photo acceptance requires the companion optional Record Files integration's successfully verified isolated fixture. Start `integrations/record-files/Start-ConsumerFixture.ps1` in that repository, keep its returned credential file private, and run:
```powershell
node scripts/ci/scanner-device-photo-e2e.mjs --serial TC22_SERIAL --fixture PRIVATE_CREDENTIAL_FILE
```
This runner accepts only that loopback fixture's expected account/process/parent contract. It does not start, stop or delete the external fixture. Its test intercepts Android camera/picker activity results with a generated image, verifies automatic camera upload and retained server association/hash, then checks pending gallery-original retention across rotation and cleanup on administrator-forced logout. No surroundings are photographed.
Device Settings includes **Orientation**: System default, Portrait, Landscape, Reverse portrait and Reverse landscape. **Portrait is the default** when no choice has been saved or the saved value is invalid. An existing explicit choice, including System default, is preserved. The choice is local to this installation and survives sign-out, server changes and restarts; it does not modify Android's global auto-rotate preference. Android can recreate the Activity when applying a choice. The host retains process ViewModels and saves unsubmitted manual scanner text across that recreation. Pending photo originals remain with the process's retained ViewModel.
The TC22 test target runs Android 14. Android's [requested-orientation API](https://developer.android.com/reference/android/app/Activity#setRequestedOrientation(int)) permits manufacturer overrides; for apps targeting API36 or later, Android16 [ignores orientation restrictions on displays whose smallest width is at least 600dp](https://developer.android.com/about/versions/16/behavior-changes-16#adaptive-layouts). The setting requests the platform policy and does not force transforms or work around those system restrictions.
+42
View File
@@ -0,0 +1,42 @@
# Echo native UI acceptance fixture
The explicit Echo scenario exercises the real Android host, TABLE login, enrollment/approval, semantic printer/media/document choices, real Echo process and H2 persistence. Its test-only backend accepts **simulated** jobs. Passing it does not qualify EMDK, barcode rendering, printer adapters or physical output.
The backend owner supplies `com.kingsrook.examples.echo.fixture.EchoHttpFixture` from the isolated optional Echo test module, launched with a free loopback port, `ECHO_FIXTURE_TYPE=echo-ui-e2e` and private `ECHO_FIXTURE_SCANNER_PASSWORD`/`ECHO_FIXTURE_ADMIN_PASSWORD`. Readiness is `ECHO_UI_E2E_READY port=N simulatedPrinting=true`. It uses in-memory H2 and no real printing/generation adapter. Root owns launch, the exact private dependency tuple and approval of the concrete APK/fixture cohort.
The credential file must stay private and contain these fields with newly generated passwords of at least 16 characters:
```json
{
"fixtureType": "echo-ui-e2e",
"simulatedPrinting": true,
"baseUrl": "http://127.0.0.1:PRIVATE_FREE_PORT/",
"username": "scanner",
"password": "PRIVATE_GENERATED_PASSWORD",
"adminUsername": "admin",
"adminPassword": "PRIVATE_GENERATED_PASSWORD"
}
```
The runner refuses normal scanner ports 57832/51631 and printing ports 18081/18086, non-loopback/decorated URLs, the wrong purpose, unsimulated fixtures, incompatible core/mobile contracts and branding other than `Echo UI E2E — simulated printing`. It requires UI API1.10.0/mobile-scanners1.4.0 and checks APK package identities. Only a selected physical Zebra TC22 and absent disposable `io.qrun.mobile.scanner.e2e`/`.test` packages are accepted. Normal and QA packages/data remain untouched. Location reporting is disabled only in the new disposable package, and no screenshots are taken.
Preparation requires no device execution:
```text
node --test scripts/ci/scanner-echo-guards.test.mjs
./gradlew :scanner-host:testDebugUnitTest :scanner-host:lintDebug :scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest
```
Only after root launches the private fixture and approves the exact cohort:
```text
node scripts/ci/scanner-device-echo-e2e.mjs --serial TC22_SERIAL --fixture PRIVATE_ECHO_FIXTURE.json
```
The native test signs in, enrolls and approves through existing helpers; opens Apps → Echo; selects the printer, media and an INTEGER Document through semantic possible-value controls, reselects the printer to verify stale dependent selections are cleared and Next is refused until reselection, then retains all three choices; injects two public synthetic values with CODE128/QR types through the E2E-only driver; checks `Print queued` and absent manual controls; rotates/recreates without another acceptance; then uses Apps/Leave and verifies the run closes. Independent reads use standard QQQ `echoScan` and read-only `echoFixtureRun` queries as the scanner with approved-device headers, retaining the real actor/device locks. No custom business verification endpoint or administrator bypass is used.
The existing OpenAPI proxy audits all observed requests and requires native cancellation. Fixed stage markers and operation IDs/counts, APK hashes and a simulation-labelled `echo-proof.json` are written under `scanner-host/build/native-device-e2e/`; credentials, serial and opaque identifiers are redacted from instrumentation output. No normal app data, global animation settings, DataWedge profile or existing forwarding is replaced. The runner removes only its own reverse and newly installed disposable packages; cleanup failures fail the run and its proof.
The user-authorized TC22 invocation completed successfully after the bounded compilation/guards. This fixture remains simulated printing, not physical printing evidence.
Current recorded acceptance: all7 stages passed on TC22, including Document selection and stale selection clearing. Root verified99 focused/host JVM tests,8 Node guards, zero lint errors/warnings, and112/112 documented native requests. Printing is simulated. The proof explicitly leaves physicalScannerQualified and physicalPrintingQualified false.
+68
View File
@@ -0,0 +1,68 @@
# Emulator testing
An Android emulator verifies login, enrollment, native navigation and manual barcode submission. The separate E2E variant also supports synthetic scan injection. An emulator does not reproduce Zebra's EMDK scan engine or physical trigger.
## Prepared Windows workstation
The `QQQ_Mobile_API35` virtual device is installed under `%LOCALAPPDATA%/Android/Avd`, with Android 15/API35 and WHPX acceleration. Local emulator testing is currently paused: the operator confirmed whole-workstation stuttering stopped when the emulator closed. Do not restart it as part of automatic local tests. The next deliberate diagnostic launch should compare software rendering while preserving the other settings:
```powershell
$env:ANDROID_AVD_HOME = Join-Path $env:LOCALAPPDATA 'Android/Avd'
& "$env:LOCALAPPDATA/Android/Sdk/emulator/emulator.exe" -avd QQQ_Mobile_API35 -gpu software -no-snapshot -no-audio
```
The previous host-GPU run logged OpenGL context restoration and external-memory import failures. Sampled host resources had ample headroom and WHPX was operational; graphics interoperability is the leading hypothesis, not a proven cause. The saved AVD now has `hw.gpu.mode=software`, with the previous config backed up locally; that renderer has not yet been tested. Software graphics retains WHPX CPU acceleration. A cold start avoids restoring old graphics state; `-no-snapshot` preserves installed apps and their data. The [Android acceleration guide](https://developer.android.com/studio/run/emulator-acceleration) recommends software rendering when host/auto graphics have problems. Preserve other settings during the comparison, observe desktop responsiveness as well as guest frame times, and stop if workstation stalls recur. Use the physical TC22 or hosted emulator CI for acceptance while local testing is paused.
The local demo uses port57832 on this workstation because other services occupy the conventional ports. Start it from the sibling `qbit-mobile-scanners` directory in another terminal, then forward the emulator port:
```powershell
pwsh -File scripts/Start-Demo.ps1 -SkipBuild -Port 57832
```
```powershell
& "$env:LOCALAPPDATA/Android/Sdk/platform-tools/adb.exe" -s emulator-5554 reverse tcp:8080 tcp:57832
```
Keep the Android server URL `http://127.0.0.1:8080/`. The browser administration URL is <http://127.0.0.1:57832/>. The demo's [local guide](https://github.com/QRun-IO/qbit-mobile-scanners/blob/develop/docs/LOCAL-DEMO.md) explains the `scanner`/`admin` accounts and password-copy helper. Open **QQQ Mobile** in the emulator. Tap the connection footer to view or copy diagnostics; it distinguishes server reachability from login and approval.
## Local interactive test
1. Create/start an API35 Android emulator in Android Studio, or use the SDK emulator installed on the development machine.
2. Build/install `scanner-host/build/outputs/apk/debug/scanner-host-debug.apk` with `adb install -r` targeting the emulator's serial if multiple devices are connected.
3. Start the companion [mobile scanner demo](https://github.com/QRun-IO/qbit-mobile-scanners/tree/main/demo) on port8080.
4. Run `adb reverse tcp:8080 tcp:8080` and use `http://127.0.0.1:8080/` in QQQ Mobile.
5. Sign in with the demo operator account. Approve the displayed device from the administrator session, then tap **Check approval**.
6. Select the mobile app, open its scan process, and submit a synthetic barcode manually. Verify **Saved scan**, and verify the server's stored record.
7. Revoke the device on the server and verify subsequent submissions are denied. Sign out and verify a new login is required.
Only the disposable E2E APK accepts synthetic broadcasts while its scanner screen is visible. Normal debug/release builds ignore this ingress and use EMDK or manual entry:
```text
adb shell am broadcast -a io.qrun.mobile.scanner.SCAN -c android.intent.category.DEFAULT -p io.qrun.mobile.scanner.e2e --es com.symbol.datawedge.data_string DEMO-001 --es com.symbol.datawedge.label_type LABEL-TYPE-CODE128
```
This validates intent parsing only; the server treats source/symbology as client-reported metadata.
## Automated checks
The GitHub **Scanner Android** workflow runs JVM tests, lint, APK assembly, the shared native UI suite, and the real-server scanner E2E on pushes and pull requests. The emulator job checks out exact core and companion qbit commits, verifies them in a fresh private Maven repository, and starts a fresh isolated demo database. [Fixture provenance](CI-FIXTURE.md) describes the source pins, runtime checks and distinction from published release dependencies. Sanitized instrumentation results are uploaded as workflow artifacts. Hosted execution is verified separately from a local run.
The scanner E2E installs **QQQ Mobile E2E**, a separate application (`io.qrun.mobile.scanner.e2e`). It drives server selection, username/password login, pending enrollment, approval, app selection, Scan Capture, manual barcode submission, server acknowledgment, independent persisted-record verification, and revocation of an already-open scan process. Administrative setup uses the real QQQ process endpoints; the companion qbit's browser suite covers their single-record and selected-bulk UI entry points. No repository or server mocks are used. Random test credentials travel through standard input into the test application's private files, are removed after reading, and are redacted from reports. The runner preserves the normal scanner package and its port forwarding.
The runner uses a test-only forwarding proxy that checks observed requests against the real server's published OpenAPI operations. It modifies only the version response for incompatible-major, missing-extension, missing-Android-channel and additive-future-version scenarios. Rejected servers must receive no credentials or requests beyond public discovery. The eight scenarios also cover the real scan flow, native administrative forms for approval/revocation/session termination/cancellation, session activity and actual screen mirroring with forced logout, and a PixelCopy pixel-content regression. Compatible scenarios use the real backend, processes and database. The audit report contains operation IDs and counts, never request headers or bodies. [SESSION-STATE.md](SESSION-STATE.md) records actual passing runs and revision boundaries. The runner wakes/unlocks the selected emulator, temporarily disables animation scales and restores their original values during cleanup. UI conditions have a 60-second deadline and instrumentation has a 300-second deadline. Set `E2E_SCENARIO=session-monitor` or `E2E_SCENARIO=pixel-copy` for focused verification.
The default suite also runs bundled-license rendering, local orientation policy checks and the diagnostics filtering/pause/clear/return UI test. Set `E2E_SCENARIO=licenses`, `E2E_SCENARIO=orientation` or `E2E_SCENARIO=diagnostics` to run one independently; these scenarios need only the scanner E2E APKs and do not start a server or establish USB forwarding. After installation and ordinary scenarios' data resets, the runner grants coarse and fine location permission only to `io.qrun.mobile.scanner.e2e`, keeping the default-on reporting permission dialog from interrupting unrelated scanner/UI automation. The session-monitor scenario instead explicitly revokes both permissions and seeds only the already-requested marker, preserving default-on reporting and its real denied-location telemetry assertion without an OS dialog. This fixture setup does not exercise the real Android permission prompt or user denial interaction and does not change permissions for the normal scanner or separate QA package.
To reproduce with Node 22+ and Java 21, first prepare the [source-pinned fixture](CI-FIXTURE.md). Use its qualified server directory below (the example assumes `.e2e-server`); an ordinary server build against the default RC.20 core does not include the nullable process fix:
```powershell
./gradlew.bat :scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest
$env:ANDROID_SERIAL = 'emulator-5554'
node scripts/ci/scanner-e2e.mjs .e2e-server
```
Set `ANDROID_HOME` (or `ANDROID_SDK_ROOT`) if `adb` is not on PATH. Linux/macOS use `./mvnw`, `./gradlew`, and their shell's environment assignment syntax. The runner refuses physical devices, uses a free local port and temporary database, and cleans up its server, forwarding and both isolated test APKs. The older setup-only `scripts/ci/emulator-smoke.sh` is retained for disposable-emulator troubleshooting; it clears the normal scanner package and should not be used on your interactive demo emulator.
## Physical acceptance still required
Test the TC22 EMDK trigger, rotation/backgrounding, retry after a lost response, revoked approval, leading-zero barcodes and repeated physical scans. Confirm DataWedge scanning works again after QQQ releases ownership. An uncertain submission must retain its event ID; a repeated physical scan receives a new event ID. See [trusted scanner acceptance](ZEBRA-SCANNING.md). Never use sensitive production barcodes in test screenshots or CI artifacts.
+34
View File
@@ -0,0 +1,34 @@
# Finish ledger for 1.0 (current: November 3, 2026)
Nothing here is a 1.0 release. History is in SESSION-STATE.md.
## Source published (not merged, not released)
- Android `f72b90d08cf29d2785cef1d5c5e7eeaf75f9afd2`: signed application code published in PR 10 (read back over SSH before this documentation update). Gitea CI 3480 succeeded: unit-test, device-test, package, secrets, gate. A clean git merge-tree with `main` b75fdac was verified; nothing was merged. Pull requests #8 and #10 remain open. This documentation update leaves runtime code unchanged.
- Server `c0e27b36fd16c00eaca3451af3df43f4deedada1`: signed and published in Gitea PR 7, including the verified QQQ 4.1.0-RC.21 dependency update and Record Files test fix.
- Generation copies: signed `aac68907e67906aeef4e0cd687d28bc98b738f8c` on `feature/echo-generation-copies` (base PR 343 head `8f473fa`), 13 paths, 21/21 manifest match, real PostgreSQL 17 migration passes. Owner integration and publication are not acknowledged.
## Implemented and installed (normal TC22 and local server 57832)
- APK `4AAB6951D3B0ABDDD4F5A8DC6790DCD84233EE08179B68DB108BF77909AB850E`: same signer, no data clear. Connected, Apps/Settings, portrait, location on and the brand-only wallpaper preview were verified. No wallpaper or company profile was applied. The device shows Echo setup, Copies 1.
- Server on 57832: UI API 1.10, mobile-scanners 1.5, printing 2.3, mobile-record-files 1.0, with the original database, accounts, approval and printer. Echo Copies per scan (default 1, range 1-100, one job per scan). Its core is not upgraded to RC.21.
## Tested
- Android: 222 JVM tests, lint 0 errors/0 warnings (2 info), debug/E2E/test APK builds, 23 Node tests, 255-file standards.
- Disposable TC22 Echo ten stages, 109/109 semantic requests; inputs and printing are simulated.
- Server RC.21 gate PASS: 377 unique tests (Generation 179, Printing bridge 16, scanner 53, demo 8, mobile Record Files 18, Echo 68, Echo demo 35), zero failures/errors/skips, Checkstyle PASS, plus 15 real admin-browser E2E on public RC.21. Earlier MySQL timeouts did not recur. Evidence: `C:/Users/james/.cache/qqq-rc21-copies-gate-076d059d8d0641d9a85874ec64008def/verified-summary.json`.
- Honest scope: this is public core RC.21 plus qualified owner inputs. The Printing-bridge tests still resolve three private `pu507` libraries through the owner Printing POM, and other owner qbits are private inputs. It is not a fully public or released cohort.
- Real Authentik desktop acceptance passes with strict TLS (nonce, A, logout, fresh B). The native harness is compiled with 8 launcher guards passing.
## Still open (none of these is claimed done)
- Generation: canonical owner integration, then public reproducible inputs for the owner qbits and a coherent published cohort.
- Source: primary CI on the final source, merge, review completion and release.
- Manual on the TC22 (operator remote): physical trigger, scan value/type, AIM, label count, rescan, photo capture/upload, GPS, projection, company profile/wallpaper deployment.
- Native Authentik on the device: temporary CA installation, PIN interaction and the real callback. No trust bypass.
- Signing and distribution: explicitly last, by user decision.
## Pointers
Runtime `C:/Users/james/.cache/qqq-local-all-qbits-current.txt`; native proof `scanner-host/build/native-device-e2e/echo-proof.json`; Generation handoff `C:/Users/james/.cache/qqq-generation-copies-handoff.md`; Authentik `docs/REAL-AUTHENTIK-ACCEPTANCE.md`.
+18
View File
@@ -0,0 +1,18 @@
# Legacy source follow-ups
The standards cleanup removes speculative inline work markers and obsolete commented-out implementations. This checklist preserves the unfinished questions in repository task tracking. None of the entries below is represented as an implemented feature or a newly reproduced defect. Changes to supported semantics require their own contract and acceptance coverage.
- [ ] `QAppMetaData`: determine whether supported native consumers require `supplementalAppMetaData`; define a typed contract and fixtures before adding it.
- [ ] `QFieldMetaData`: inventory field behavior metadata actually needed by native controls; avoid an untyped `List<Any>` placeholder.
- [ ] `QInstance`: define native report support and a typed report contract before exposing report metadata.
- [ ] `QTableMetaData`: inventory omitted table fields against the supported semantic UI API and add required fields with sanitized real fixtures.
- [ ] `QWidgetMetaData`: inventory omitted widget fields against supported native widgets and add required fields with fixtures.
- [ ] `ProcessComponentHelpText`: decide whether preview/expand behavior is part of the supported contract; test it if introduced.
- [ ] `ProcessComponentWidget.buildProcessComponentWidgetState`: define metadata-driven header/body/footer placement before restoring any list-position heuristic. The deleted implementation was inactive.
- [ ] `ProcessComponentWidget.evaluateWidgetBlocks`: reconcile legacy `INPUT_FIELD` value propagation with the native input renderer; do not restore commented web-client code without contract coverage.
- [ ] `CompositeWidgetBlock.Modal.controlCallback`: confirm whether the semantic callback means toggle or an explicit target visibility; test repeated callbacks against that contract.
- [ ] `ProcessViewModel.closeProcessCallback`: review current guarded navigation and cancellation coverage before changing reverse-navigation notification. The old comment predates the guarded navigation implementation and does not establish a current failure.
- [ ] `ProcessViewModel` widget event handling: verify permitted action-code validation against server metadata; define a fail-closed native check if the supported contract requires it.
- [ ] `QViewModel.loadQInstance`: derive frontend name/version from the declared client build identity instead of the existing fixed identification strings, with compatibility request coverage.
The removed alternate network return and speculative session-test wait snippet are obsolete inactive implementations, retained in Git history rather than executable source. Their deletion changes no runtime behavior. Runtime, provider and physical-device acceptance gates remain tracked in the main task list and readiness documents.
+19
View File
@@ -0,0 +1,19 @@
# Native navigation and OAuth2 acceptance
On October 31, 2026, the separate sample application test package ran on the physical Zebra TC22. The latest terminal instrumentation summary was `OK (21 tests)`, elapsed 53.482 seconds, with `INSTRUMENTATION_CODE: -1` (the normal successful Android instrumentation result). No operator identifier, barcode, credential or raw device log is retained here.
The selected classes were NavigationTests, SemanticInputTests, SessionTests and OAuth2LoginTests. They cover the signed-out drawer, Back and guarded app/process navigation, secondary Settings/Diagnostics, input/account behavior, and seven OAuth2 cases:
- An advertised browser flow creates exactly one semantic exchange and opens the authorized app catalog. The actual AppAuth request uses S256 and exactly one `prompt=login` parameter.
- Browser cancellation does not exchange a code or offer a password fallback.
- A mismatched browser state never exchanges a code.
- A cancelled attempt cannot be revived by a late successful browser result.
- A cancelled exchange discards a known late session at its original server without adopting it.
- Missing native advertisement offers neither password nor browser login.
- A mismatched discovery issuer is rejected before browser launch.
The initial run passed 19 cases and exposed Back during drawer-opening animation. The handler now consumes Back while the drawer is open or targeting Open; the test helper settles the UI before issuing hardware Back. The rebuilt suite passed all 20 cases. The follow-up bounded build also passed 102 JVM tests, scanner lint, host debug/E2E APK assembly and native-test compilation in 1m3s (266 tasks, 34 executed).
The latest 21-case run adds the unconfirmed-scan regression: a lost response and malformed receipt retain the original event; Stay preserves it; failed cancellation leaves the operator a choice to retry or explicitly Leave anyway; the final local exit reaches the app and Apps catalog without an extra scan submission or cancellation. The bounded fix build passed all 46 mobile JVM tests, scanner lint/debug APK and sample/test APK assembly in 18 seconds. Pending-photo and active-request guards remain enforced by regression tests. The native fixture simulates response and cancellation failures; it does not claim a server rollback.
This is actual handheld execution with a synthetic QQQ repository, injected HTTPS discovery metadata and an instrumentation monitor returning AppAuth activity results. It does not establish a live Authentik login, an actual browser redirect through the exported callback activity, browser-authorization rotation/process-death behavior or configured shared-operator reauthentication. Those remain deployment acceptance gates in [AUTHENTICATION.md](AUTHENTICATION.md). This run did not replace the normal operator scanner app or modify its data. No production signing or release delivery is implied.
+70
View File
@@ -0,0 +1,70 @@
# Mobile navigation and operator experience
Status: implementation and native acceptance in progress. This specifies the requested behavior; it does not claim the current APK already implements every row.
## Information hierarchy
The primary hierarchy is **Apps → selected app → running process**. The Apps catalog lists only server-authorized business apps. App pages list their server-defined processes. Client settings, troubleshooting and account actions are not business apps and do not belong in that catalog.
Every screen provides one **hamburger navigation drawer**, a clear current app/process title, and a separate **Account** action. The drawer's primary section contains **All apps** and the server-authorized apps; Settings and Diagnostics remain fixed at its bottom while the app list scrolls. There is no separate Apps toolbar button or Switch app overflow action. The hamburger remains accessible alongside Back inside an app or process. Before login or device approval, the drawer exposes Settings and Diagnostics but hides business destinations. Use one authoritative navigation stack; toolbar Back and Android Back must call the same navigation policy. Do not maintain a second mutable title/history stack. App switching replaces the active app branch so Back cannot unexpectedly return to an abandoned process.
| Layer | Contents and entry |
| --- | --- |
| Primary navigation | Apps catalog, chosen app, its processes. All apps and authorized app entries are reachable through the hamburger drawer from every approved authenticated screen. |
| Process actions | Server-defined submit/next/finish actions; contextual Cancel/Close. A previous business step exists only when supported by the server contract. |
| Account | Current user and Sign out. Explicit user sign-out is distinct from forced session expiry. |
| Settings | Current server/customer, device name/short name, device permissions, supported operator preferences. Changing server requires leaving work and signing out of the old server. |
| Diagnostics | Server reachability details, recent safe logs, retry/check connection and app/version information. Reachable from the connection footer and secondary menu. |
| Administration | Approval and fleet management remain in QQQ administration. A handheld pending approval shows its short name and administrator instructions. |
The default-on foreground location reporting toggle, permission recovery and actual provider/fix status belong under device settings. Android location permission is requested once when an approved signed-in session first becomes foreground with reporting enabled; denial leaves scanning usable. Camera permission is requested when the operator chooses Take photo. A live viewing indicator remains visible while an administrator is viewing the app. The app provides no pause or stop viewing control, including in Settings; Android's system consent and stop controls remain available for optional whole-device sharing.
The owner also requires a device-local Orientation preference in Settings: System default, Portrait, Landscape, Reverse portrait and Reverse landscape. Persist the selection across app restarts and apply it across login, menus and processes. Use Android's activity orientation policy without changing system-wide rotation settings. Rotation or a policy change must preserve drafts and pending upload identity. Licenses belong in the same secondary Settings layer and must remain readable offline.
## Entry and exit states
1. Initial setup accepts a server origin, validates compatibility before credentials and obtains customer branding. Errors provide Retry, server settings and diagnostics. Release builds require HTTPS.
2. Login clearly identifies the customer/server, labels username/password, supports keyboard Next/Done and a reachable Sign in action on TC22-sized screens. Submission shows progress and prevents duplicate requests. Errors preserve username, never log passwords and never expose protected screens.
3. Pending/revoked enrollment shows a friendly device name/short name, the approval state, Check approval, Sign out and secondary diagnostics/settings. It does not show the authorized app catalog until approved.
4. Successful login and approval land at Apps. Zero available apps has an explicit empty state, Retry and account/settings access. One available app still has a discoverable Apps destination.
5. Explicit Sign out asks for confirmation, with active-work consequences if applicable. Cancel leaves navigation/session unchanged. Confirm cancels or reconciles active work, ends the mobile session, clears sensitive client state and returns to login. Device enrollment and server selection survive ordinary logout.
6. Remote termination or expired authentication immediately blocks protected content/capture and returns to login with a clear reason. It must not wait for a user confirmation or leave stale screens reachable through Back.
## Back and switching behavior
| Current state | Toolbar Back / Android Back | Drawer All apps / selecting another app |
| --- | --- | --- |
| Apps catalog | No misleading disabled Home arrow; Android Back uses normal platform exit/background behavior without signing out. | Stay at Apps; repeated taps do not add duplicate routes. |
| App menu | Return to Apps. | Return to Apps or replace the app route. |
| Completed process | Close to the containing app; no cancellation request for completed work. | Close and go to requested destination. |
| Active process, including scanner | Ask to leave the process. Stay preserves work. Confirm uses native cancellation then navigates. Saved scans remain saved; never imply Cancel undoes committed records. | Same leave policy, remembering the requested destination. |
| Initializing process | Same policy; cancellation intent survives a late process UUID and cancels that process before navigation completes. | Same policy. |
| Submission/upload in flight or pending photo | Keep the operation identity and block leaving until the request settles or the pending photo is resolved. | Same policy; no hidden background second process. |
| Settled scan with missing acknowledgment | Warn that the scan may already be saved. Stay retains the original event for an explicit same-event retry; Leave process stops retrying and requests server cancellation. Retain the event if cancellation fails, and discard it only after confirmed cancellation. | Same explicit confirmation and cancellation policy. |
| Cancellation failed | Stay retains work; retry cancellation or explicitly choose Leave anyway. Warn that the server process may still run and the scan may already be saved. Leave anyway discards local retry state and applies the requested destination without another server request. In-flight and pending-photo guards still apply. | Same second-stage explicit choice; never an automatic fallback. |
| Settings/diagnostics/account surface | Dismiss the surface, preserving current app/process and draft. | Close secondary surface, then apply the normal primary navigation policy. |
The top-level Back arrow means navigation out of the current process, not undoing a scan or automatically moving to a previous business step. Label the leave dialog accordingly, for example **Leave Scan Capture?** with **Stay** and **Leave process**. Completed scans stay saved. For multi-step forms, render a server-supported previous-step action within the process when available; never guess it from an arrow or step name.
Opening the drawer preserves the current process and draft. Android Back or tapping its scrim closes the drawer without navigating or cancelling work. Selecting All apps or another app then applies the same guarded leave policy as toolbar Back. Keyboard Back first dismisses the keyboard where Android normally does so. Dialog Back dismisses the dialog as Stay. Repeated taps while cancellation/sign-out is in progress must not launch duplicate work. Failed cancellation remains visible with retry/reconciliation; it must not silently orphan a live process. Settings changes that affect identity or server use the same guard as app switching.
## Small-screen and scanner usability
Use labeled actions with accessible names, at least 48dp touch targets, readable status and visible focus. Do not disable keyboard focus on navigation controls. Keep primary submit actions reachable above the IME or after its Done action; long forms scroll without hiding the last field or action. Titles may truncate visually but retain meaningful accessibility text. Diagnostics should show the server connection, not just whether Android has internet access. The footer is persistent across login, approval, catalog and processes and opens connection details.
## Required native acceptance
- Login → approval → Apps → app → process; verify user-facing titles and selected navigation at each stage.
- Toolbar Back and Android Back both leave a process through the same confirmation; Stay preserves field values and makes no cancellation request; Leave cancels exactly once and returns to the app.
- The hamburger remains available beside Back inside processes; duplicate Apps and Switch app controls are absent. Drawer dismissal preserves drafts and makes no cancellation request. All apps and direct app switching use the same guard and land at the requested destination; Back never resurrects the old process.
- Signed-out and pending-approval drawers expose Settings and Diagnostics without business navigation. Secondary entries stay visible below the scrolling app list.
- Completed result closes without cancel; initializing process cancellation handles its late UUID; uncertain submission does not replay an ordinary mutation or discard retry identity.
- A lost scan response or malformed receipt offers an enabled Leave process action after the request settles. Stay preserves the event; confirmed leave cancels once without resubmitting. Failed cancellation retains the event and retry option. In-flight requests and pending photos still block leave.
- Failed cancellation exposes Leave anyway only after a real attempt. Stay retains the event; cancellation retry remains available; explicitly leaving anyway clears local state once without another mutation. Verify the fallback cannot bypass in-flight requests or pending photos.
- Settings, account and diagnostics open/close without navigating away from or submitting the process; server change uses the leave/sign-out guard.
- Explicit sign-out Cancel preserves work; Confirm clears protected content and returns to login; fresh login returns to Apps with enrollment retained.
- Remote session end bypasses confirmation and prevents stale Back navigation/capture.
- Empty catalog, single app, multiple apps, short viewport, large font, keyboard navigation, IME dismissal, rotation and Activity recreation retain coherent state.
- Photo capture/upload adds matching leave/cancel/reconciliation cases once its integration is available. Physical TC22 acceptance includes hardware Back and trigger behavior after app switching.
Automated tests must exercise actual navigation and request counts/state, not only button visibility. Companion real-server tests verify cancellation/session changes and persisted scans. Physical tests qualify handheld ergonomics separately.
+34
View File
@@ -0,0 +1,34 @@
# Physical TC22 acceptance — November 1, 2026
This records development-package evidence, not production release acceptance. The normal application was updated in place after signer verification and private-data backup. The emulator remains off.
## Current installed checkpoint
Normal portrait-default APK `C89161C522AA7DFFE09C009ABDE769C1BADB7E6C64BFCE699857062A1CEC6340` is installed, backed by all eleven hosted checks at7de. One subsequent trusted EMDK Code128 event is verified after that update, in addition to the eight deliberate priorF3AF events below. Current leading-zero QR remains pending; publishing/decoding its QR fixture is not trigger-to-save acceptance. Normal server is still coreecf/serverb14 API1.9/mobile1.1 on57832; whole-device1.2 source is not a normal-runtime upgrade.
## Earlier F3AF checkpoint
| Component | Exact tested checkpoint |
| --- | --- |
| Development APK SHA-256 | `F3AF638E34AA0B6FB44D789362BED2838202EDAF681DF775F2A5FAD3AB3E9FAA` |
| Production source checkpoint | `e04fdb2d21d4f84253f12137af106bbcb0344a19` |
| Backend core | `ecf0f550a0357cec6387c2b54efca128a405a629` |
| Running scanner server | `b14d925b741e815d1cfa4f62301f19207f25b03f`, UI API1.9/mobile-scanners1.1 |
Direct display inspection confirmed sign-in, the Apps menu, Scan Capture and “Scanner ready.” The operator then reported “Saved scan.” A direct, ownership-guarded backend query verified eight new `QQQ-TEST-123456` records with `scanSource=zebra-emdk`, `symbology=CODE128`, TC22 model attribution and eight distinct event identities. Their device capture times run from `2026-11-01T11:02:17.572512Z` through `2026-11-01T11:02:40.208343Z`. Device capture and server receipt times are separate clock observations.
This proves a trusted hardware trigger-to-save positive control. The operator confirmed the captures came from multiple deliberate trigger pulls. Historical DataWedge records were excluded from this result. The normal database has eleven records at the observation, including its three preserved historical scans; no disposable instrumentation was run over this walkthrough.
| Required behavior | Current proof |
| --- | --- |
| Code128 trigger to durable scan | Eight priorF3AF events plus one installedC891 event verified on EMDK, with operator Saved scan and matching backend records. |
| Leading-zero QR `001234567890` | Fixture independently decoded; current physical operator/backend confirmation pending. Historical DataWedge QR does not satisfy this gate. |
| Separate repeated trigger intent | Eight distinct events observed; operator confirmed multiple deliberate pulls. |
| Rotation/recreation, background/resume, scanner ownership restoration | Physical matrix pending; native synthetic/hosted evidence is separate. |
| Connection failure, uncertain receipt/retry, leave/cancel and switching apps | Operator walkthrough pending; automated regressions remain separately scoped. |
| Remote termination and fresh login | Current hosted verification passes; current physical operator checks remain pending. |
| Actual projection, camera/gallery and GPS | Separate consent/provider/runtime gates; not implied by barcode capture. |
The reported disappearance before sign-in was inspected directly: the process remained alive/resumed/focused, the crash buffer was empty and the display showed connected sign-in. No app crash or product fix is established by that observation. The desktop mirror was black; direct local ADB screenshots provided the usable display evidence.
The later portrait-default update produced development APK `C89161C522AA7DFFE09C009ABDE769C1BADB7E6C64BFCE699857062A1CEC6340`. Four focused policy tests, debug lint (zero errors/warnings) and debug assembly pass. It was installed in place with a matching signer and another private-data backup. Direct Android inspection confirms the normal scanner Activity resumed with `SCREEN_ORIENTATION_PORTRAIT`; the display shows its signed-in Apps menu. Missing/invalid settings now default to portrait; explicit saved choices remain intact. The eight-event observations in the earlier checkpoint belong toF3AF; the subsequentC891 Code128 positive control is summarized above. Current QR/lifecycle checks remain pending.
+68
View File
@@ -0,0 +1,68 @@
# Real Authentik native acceptance (opt-in, physical TC22)
This is the only test that runs the Android OAuth2 callback against a real provider. It is skipped by default
(`RealAuthentikAcceptanceTest` needs `-e realAuthentik true`) and is never part of the synthetic E2E scenarios.
Until it passes on the device, [AUTHENTICATION.md](AUTHENTICATION.md) makes no claim of an Android Authentik round trip.
## What it exercises
The unmodified flow: `oauth2.signIn` -> real AppAuth external browser -> the real provider -> the exported
`OAuth2RedirectActivity` (`io.qrun.mobile.scanner.e2e.oauth://callback/oauth2redirect`) -> the real QQQ code exchange ->
device pending -> real administrator approval (`mobileDeviceApprove`) -> catalog; then operator A's **Sign out of company**
(RP-initiated, through the browser) and a fresh login of operator B without clearing any browser state. It asserts the
principal QQQ shows (`Current User: ...`), exactly one ACTIVE session for the device, A's session no longer ACTIVE, B's
different, an optional exact server actor, and that no password form is ever offered. It injects no callback, ActivityResult,
token or discovery document.
## Trust and transport
- `scanner-host/src/e2e/res/xml/network_security_config.xml` (E2E package only) keeps system trust and the existing cleartext
`127.0.0.1` fixture traffic, and additionally lets the literal host `localhost` trust a **user-installed** CA over HTTPS only.
Nothing is pinned or bypassed; debug, qa and release builds are unchanged.
- The temporary fixture CA must be installed by the person holding the device, through Android settings (a screen lock/PIN is
required), and removed afterwards. Neither this test nor the launcher installs, probes or changes trust. The browser must
also trust that CA, otherwise it shows a certificate warning: do not continue past it.
## Prerequisites
The provider fixture and QQQ are running (QQQ `https://localhost:18443`, Authentik `https://localhost:19443`) with the exact E2E
callback registered for login and post-logout; a physical TC22 with `ANDROID_SERIAL` set only in the environment, unlocked and
awake; the disposable packages absent; and a private config file **outside the repository** (it holds a live administrator
session cookie):
```json
{
"fixtureType": "real-authentik-android",
"qqqOrigin": "https://localhost:18443",
"adminCookie": "<Cookie header value of an admin QQQ session; never printed>",
"operators": {
"A": { "display": "<exact Current User text for the first operator>", "actor": "<optional server actor value>" },
"B": { "display": "<exact Current User text for the second operator>", "actor": "<optional server actor value>" }
},
"sessionActorField": "<optional mobileSession column holding the actor; omit to skip>"
}
```
## Run
```text
./gradlew :scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest
node scripts/ci/real-authentik-android.mjs --config <private-config.json> --confirm-temporary-ca-installed
node --test scripts/ci/real-authentik-android.test.mjs # guard tests, no device needed
```
The launcher refuses emulators and any device that is not a Zebra TC22, refuses a config inside the checkout or with a different
origin, and refuses if `io.qrun.mobile.scanner.e2e` or its `.test` package already exists (it never uses `install -r`). It adds
`adb reverse` for `tcp:18443` and `tcp:19443` only when absent (never rebinding; a differing existing mapping is an error),
never touches `tcp:8080` or any other mapping, never reads or changes the normal `io.qrun.mobile.scanner` package or its data,
pushes the config through `run-as` stdin (never to shared storage), and afterwards removes only the pushed file, the two
disposable packages and the reverse mappings it created. Output is fixed stage text and a PASS/failure sentence: no serial, URL,
cookie, token or credential. The operator types provider credentials in the browser when an `ACTION:` line appears (bounds: 45 s
for the browser to appear, 600 s per login, 300 s for company logout, 40 min total).
## Limits
No browser automation is included, so the operator enters credentials. The test cannot verify CA installation; a missing CA
shows up as the browser never opening within the bound. Process death during the browser step loses the in-memory attempt by
design; rerun. A pass proves this provider/server/package combination only; it is not production-provider, release-build or
EMDK trigger qualification.
+17
View File
@@ -0,0 +1,17 @@
# Release automation review — November 1, 2026
Independent read-only review of Android revision `5ba87f49d2fc5ab7f52566727d0d2d147d9b0c9f` found no actionable correctness or security finding in these three files. The review did not execute a production release or inspect GitHub environment settings.
| File | SHA-256 |
| --- | --- |
| `.github/workflows/release-scanner.yml` | `61D581A452D00E53D83F9A61AE1702C2BA349833481B9B23CDE955B2C204426A` |
| `.github/workflows/scanner.yml` | `E6F792B4C1CDDFD3621E1CFD2009EF7F610C3C546357F83428DE5703B0C289F0` |
| `scanner-host/build.gradle.kts` | `AAF0DEAE4B653E3455B83D2CAE190F7F06D100721CBB9A471F23586F0DB5EFC6` |
The reviewer checked reusable verification, native testing on tag pushes, read-only tokens until publication, environment-scoped signing inputs, main ancestry, monotonically increasing Android version codes, temporary keystore cleanup and complete Gradle signing configuration with no debug-signing fallback. Signed APK inspection checks certificate, package, version and non-debuggable status. Publication verifies asset hashes and resolves the remote tag to the verified source commit.
The tag check is point-in-time. Protected tags, authorized maintainers, environment reviewers and immutable-release settings require separate inspection/configuration; workflow YAML does not establish them. The signed release is rebuilt separately from the tested debug/E2E variants. Its lint and signature/manifest checks do not replace testing the actual signed APK, including preserving upgrades and device behavior.
Read-only GitHub inspection on November1 confirmed strict required checks on `main`, enforced for administrators, and disabled force pushes/deletions. Required pull-request reviews were absent. The visible ruleset list contained the main code-scanning rule and no tag ruleset; the `scanner-release` environment lookup returned404 to the current connection. Release-environment/protected-tag readiness therefore remains unproven and must be resolved before loading signing secrets.
Signing and distribution remain the owner's final decision. No key, secret, tag, GitHub protection, production release or handheld state changed during this review. See [release prerequisites](RELEASING.md).
+43
View File
@@ -0,0 +1,43 @@
# Scanner APK releases
Status: **no distribution channel is selected.** Everything below describes a candidate GitHub-based APK workflow that is prepared but not chosen; the primary forge is Gitea (`git.kof22.com/QRun-IO`) and GitHub is a shadow/archive, so treat the GitHub environment, tag and release steps as design notes to be re-decided, not instructions to follow now. The signing identity, complete native acceptance, final security review and first release are not yet qualified. Current installed demo APKs use a development certificate. Do not label them production releases.
The [November 1 independent automation review](RELEASE-AUTOMATION-REVIEW.md) found no actionable finding in the exact reviewed workflow/build files. External GitHub protections, the production identity and actual signed-device acceptance remain unverified.
Owner decision, October 31: leave signing and distribution until the final step because the distribution channel has not been chosen. Do not generate signing keys, populate production signing secrets, create release tags or treat the candidate GitHub APK workflow as a selected distribution channel. Complete implementation, audits, tests, documentation and ordinary CI first; then return to the owner for the distribution decision.
The shared library keeps its existing main-only CircleCI publication. The candidate scanner workflow uses a separate `scanner-v<semantic-version>` tag and GitHub release (not selected). One APK serves customers through the selected server's runtime branding. The application ID and launcher identity remain `io.qrun.mobile.scanner` / QQQ Mobile.
## Release prerequisites
- Merge only after the exact source passes the shared library, host, real-server native and companion browser tests, plus the independent security review. Update the immutable companion-server revision in `scanner.yml` to the qualified server commit first.
- Complete the navigation and physical TC22 acceptance matrix, camera/Record Files integration qualification, dependency attribution and production HTTPS/authentication/storage checks documented in the project TODO and companion readiness report.
- Establish an organization-owned release keystore and backed-up recovery process outside Git. Keep that signing identity for upgrades. Configure the `scanner-release` GitHub environment before adding secrets: restrict deployment tags to `scanner-v*`, protect release-tag creation/update/deletion with repository rules, and limit authorized release maintainers. Store `SCANNER_KEYSTORE_BASE64`, `SCANNER_STORE_PASSWORD`, `SCANNER_KEY_ALIAS`, and `SCANNER_KEY_PASSWORD` in that environment, not repository-wide secrets. A workflow environment declaration alone does not create these protection policies; actual GitHub configuration must be reviewed before loading production keys. Never place secrets in Gradle properties committed to source or pass passwords on the command line.
- Set repository variable `SCANNER_CERTIFICATE_SHA256` to the intended certificate's SHA-256 digest, 64 hex characters without separators. The build compares the resulting APK signer to this independently configured public fingerprint.
- Set repository variable `SCANNER_VERSION_CODE` to a positive integer greater than every previously distributed scanner version code. Android's maximum is 2100000000. Release workflows are serialized across tags and compare the selected code with every published `scanner-v*` release's `BUILD.txt`, including prereleases. Missing or malformed prior build evidence fails closed. Account for any APK distributed outside GitHub manually as well; changing the display version alone is insufficient.
## Build and publication
Create the immutable `scanner-v1.0.0` tag only after the prerequisites pass. Use a prerelease suffix for a qualified release candidate, for example `scanner-v1.0.0-rc.1`. Do not move a published tag or replace released APK bytes.
Push one release tag at a time and wait for its workflow to finish. GitHub's default concurrency group serializes execution but retains only one pending run; it is not a durable multi-release queue. The workflow reuses the validated version code during signing and rechecks the remote tag's commit immediately before publication. Immutable tag protection remains necessary to close concurrent tag-change races.
The tag workflow first calls the existing scanner verification workflow, including native emulator and real-server tests. Before signing, it verifies the tag's commit is an ancestor of protected `main` and enforces the increasing version code. It then builds a non-debuggable release APK, runs release lint, verifies its signature against the expected certificate, verifies package/version metadata and writes checksums and the source commit. Only the final publication job receives repository write permission. A failed verification or absent signing configuration prevents publication. The temporary keystore is removed at the end of the signing step. Signing passwords stay in masked environment variables and never become workflow outputs or artifacts. Repository/environment protection is essential because a maintainer able to change trusted workflow code could otherwise remove its in-workflow checks.
The candidate APK release contains `qqq-mobile-<version>.apk`, `SHA256SUMS`, `signature.txt`, `BUILD.txt`, `aboutlibraries.json`, `LICENSE` and `NOTICE`. The notices are generated from the release runtime and also packaged for offline access in Settings. Download and verify these assets, then qualify the actual signed bytes on a device before customer rollout. Checksums establish artifact integrity; they do not replace signer verification or functional acceptance. Signing/distribution remains deferred by the owner until the final step.
For local release builds, supply the four `SCANNER_*` signing environment variables (use `SCANNER_KEYSTORE` for the local keystore path, not the CI-only base64 variable), then run:
```text
./gradlew :scanner-host:lintRelease :scanner-host:assembleRelease -PscannerVersionName=1.0.0-rc.1 -PscannerVersionCode=2
```
Without signing variables, local release assembly produces an unsigned APK for inspection. Partial signing configuration fails immediately. Debug/E2E builds retain their existing development signing and default version unless explicitly overridden. Unsigned and debug APKs must not be distributed as production releases.
## Upgrade and Zebra rollout
Android accepts an in-place application update only with a compatible signing identity and version code. The existing development-signed TC22 installation cannot simply be replaced by an unrelated production certificate. Preserve its test evidence and explicitly plan that migration: reinstalling removes private enrollment/session state and requires enrollment/approval again. Do not silently uninstall the operator's application to make a release install work.
For Zebra devices, qualify the device-provided EMDK scanner path described in [Zebra scanning](ZEBRA-SCANNING.md). Production must exclude the synthetic scan receiver and compile-only SDK implementation. Verify actual trigger capture, repeated and empty reads, rotation/reacquisition, background/leave/logout release, and restoration of scanner ownership to other applications. The production path requires no DataWedge profile changes; do not persistently disable DataWedge or treat its recipient signature check as sender authentication. Complete the separately signed ordinary-UID broadcast rejection test against the isolated production-path QA build, then qualify Back/Apps navigation, session termination, reconnect behavior and photo capture using the actual signed release APK. Subsequent customer updates should preserve server selection and device enrollment and keep the same signing identity. Customer branding comes from server metadata and does not require rebuilding the APK.
If a delivery path is later chosen, an APK from it could support manual or MDM distribution. Play distribution, managed enterprise rollout, signing-key rotation and a production backend deployment need their own qualified configuration; this workflow does not claim to establish them.
+36
View File
@@ -0,0 +1,36 @@
# October 31 client security review
The deep static review completed against published client revision `d1de0bf`.
It reported one medium and five low findings. Coverage is partial: external
server/provider/dependency implementations and some bundled binary implementation
review were outside the completed scope. This is not production release clearance.
| Finding | Severity | Current verification |
| --- | --- | --- |
| Delayed password completion after logout/server change | Medium | Fixed origin/generation binding and deferred cookie adoption; HTTP and native delayed-response cases pass. |
| Sample release cleartext traffic | Low | Debug-only HTTP environments/manifest override; packaged release denies cleartext. |
| Auth0 provider failure preserving local QQQ login | Low | Local sign-out is independent, bounded and idempotent; failed-provider, restart and remount cases pass. |
| Unauthenticated external scan broadcasts | Low | Trusted optional EMDK adapter and production receiver removal verified by actual DEX inspection,51 scanner JVM tests and an ordinary-UID/distinct-signer TC22 case: eight forged broadcasts cause zero process/record mutations; manual positive control succeeds. Actual physical-trigger positive control remains required before closing the complete scanner acceptance; normal operator installation is unchanged. |
| Legacy Auth0 callback crossing server origins | Low | Origin-bound callback/exchange lifecycle, one-use exchange and discarded-session cleanup; HTTP/native cases pass. |
| Sample session backup | Low | No-backup storage, disabled backups and explicit cloud/transfer exclusions; native storage and packaged release checks pass. |
The qualification batch passes 109 JVM tests (30 core, 46 mobile, 33 scanner),
scanner lint with no errors or warnings, debug/native-test APK assembly, and
unsigned sample release assembly. A physical TC22 run passes all 32 selected
navigation, input, account, synthetic OAuth/Auth0 lifecycle and host-storage cases
in 117.325 seconds. Ten new lifecycle cases cover delayed completion, duplicate
callbacks, failed provider logout, overlapping logout and logout-screen remounting.
These use synthetic repositories/browser callbacks; they do not prove a real
configured identity-provider round trip.
Packaged release inspection confirms backup and cleartext disabled, plus all ten
cloud/device-transfer exclusions. The wrapper now matches the official Gradle 8.7
checksum; see [provenance](WRAPPER-PROVENANCE.md). The new scanner debug APK hash is
`8c4d3886b4e994759f909677849dae26a333f476a036457c9ea76fe4566c512a`.
It has not replaced the operator's installed scanner app in this checkpoint.
Separate live dependency review identified affected Guava in the server's tested
classpath. A patched source pin is undergoing isolated consumer qualification;
the client APK does not contain that Guava artifact. Actual Authentik, trusted
physical scanner input, immutable Record Files publication and final deployment
and signing gates remain open. See [TODO](TODO.md) and [release prerequisites](RELEASING.md).
+309
View File
@@ -0,0 +1,309 @@
## November 3 — verified widget-state reset fix
`ProcessViewModel.reset()` now clears `widgetState` so last scanner feedback no longer carries into a new run. The regression proves ordinary step updates retain feedback and reset clears it. Root verified all 57 mobileapp JVM tests (zero failures/errors/skips), scanner lint (zero errors/warnings), debug APK assembly and the 255-file standards gate. Prior head `9cf789c` passed hosted CI3553 in 16m22s; this fix still needs its own hosted checks and normal PR10 → PR8 → main integration. The installed APK and manual acceptance are separate gates; signing/distribution remains deferred.
## November 3 — current checkpoint (verified; supersedes all older entries below)
- **Source:** `f72b90d08cf29d2785cef1d5c5e7eeaf75f9afd2` is the signed application-code commit published in PR 10, independently read back over SSH before this documentation update. Gitea CI 3480 **succeeded** on it (unit-test, device-test, package, secrets and gate; root read the UI). Root also verified a clean git merge-tree of `f72b90d` with actual `main` b75fdac and merged nothing. Pull requests #8 and #10 remain open; no 1.0 is released. This documentation update leaves runtime code unchanged from CI-passing `f72b90d`.
- **Installed:** the updated normal APK `4AAB6951D3B0ABDDD4F5A8DC6790DCD84233EE08179B68DB108BF77909AB850E` is installed with the same signer and no data clear. Connected, Apps/Settings, portrait, location on and the brand-only system wallpaper preview were verified. No wallpaper or company profile was applied.
- **Fresh verification:** 222 JVM tests, lint 0 errors/0 warnings (2 info), APK builds, 23 Node tests, 255-file source standards pass. A fresh disposable TC22 Echo ten-stage run passed 109/109 semantic requests (simulated inputs and printing).
- **Authentik:** fresh real desktop acceptance passes (nonce rejection, A login, logout with old-cookie replay rejected, fresh B without clearing cookies, logout) against the same isolated TLS fixture. The opt-in native harness is integrated and compiled, and its 8 launcher guards pass; the on-device run still needs the temporary CA and PIN interaction. No trust bypass. The normal server on 57832 is unchanged and ready.
- **Server side:** server `45f9cbd` is signed and pushed (Gitea PR 7); the public QQQ 4.1.0-RC.21 source update is prepared for the next signed commit. The final RC.21 gate passed: 377 unique tests (Generation 179, Printing bridge 16, scanner 53, demo 8, mobile Record Files 18, Echo 68, Echo demo 35), zero failures/errors/skips, Checkstyle PASS, plus 15 real admin-browser E2E on public RC.21; the earlier MySQL timeouts did not recur. This is public core RC.21 with qualified owner inputs (three private `pu507` libraries still resolve through the owner Printing POM), not a fully public or released cohort. The normal runtime core was not upgraded.
- **Still open (not claimed):** Generation canonical owner integration and public reproducible owner inputs; primary source CI, merge and release; manual TC22 trigger, value/type, AIM, label count, rescan, photo, GPS, projection and profile checks (operator remote); native Authentik CA, PIN and callback; signing/distribution, explicitly last. The normal TC22 shows Echo setup, Copies 1, Connected.
## November 3 — final client installed; Authentik harness integrated (earlier; superseded by the checkpoint above)
The verified debug APK 4AAB6951D3B0ABDDD4F5A8DC6790DCD84233EE08179B68DB108BF77909AB850E is installed on the normal TC22 with the same signer and install -r. Existing sign-in, device approval and Connected status survived. Navigation, portrait setting, foreground-location toggle and the brand-only system wallpaper preview were verified; wallpaper was not applied. Feedback, managed configuration and AIM code are now installed, with physical trigger/fidelity still unqualified.
Fresh verification passes 222 JVM tests, lint zero errors/warnings, debug/E2E/test APK builds, 23 Node tests and the 255-file source standard check. The updated disposable TC22 Echo suite passed all ten stages with every request matching semantic operations; simulated input/printing only. The actual Authentik desktop test passed again on November 3 with strict TLS: nonce rejection, operator A, company logout, old-cookie rejection and fresh operator B. The opt-in real Authentik Android harness is now in the repository and compiles; native execution awaits temporary CA installation/device interaction. No normal app/server trust change was made.
Echo server source is signed and published at 45f9cbd in Gitea PR7. Generation copies now has an isolated canonical Git integration, with additive PostgreSQL migration verified on real PostgreSQL17. Shared source/dependency publication and hosted CI remain separate gates; no production 1.0 release or physical print claim. Earlier installed-APK/uncompiled-harness notes below are historical.
## November 2 — Echo copies live and native acceptance complete
Echo setup now exposes optional **Copies per scan**, default 1, range 1–100. The server pins it for the run and submits one job with that copy count for each scan. The normal TC22 is connected and left on the fresh setup screen showing 1. Its installed APK remains D8109C61E71660D2195337DA988F7F692CC4A0DBE95F61230930D6549CAB4D9B; no client upgrade was needed for this generic semantic field.
Disposable TC22 Echo acceptance passed ten stages and 116/116 documented requests, including default 1, rejecting 0/101, selected 3 across two scans, recreation and Apps/Leave. Inputs and printing are simulated; physical label count is unconfirmed. The combined source passes 222 JVM tests, lint with zero errors/warnings, APK compilation and 15 Node tests. New scan-feedback, managed-profile/wallpaper and AIM source is not installed on the normal app.
The existing local server on port 57832 now advertises mobile-scanners 1.5 and mobile-record-files 1.0, retaining the original database, approval, accounts and printer settings. Server/owner gates passed 377 unique tests across the qualified module runs. Generation copies changes remain in the isolated owner handoff, awaiting canonical adoption/publication. Physical trigger/print/rescan, camera/GPS/projection and native Authentik remain open; the operator is remote and temporary CA installation is unconfirmed. Signing/distribution stays last. Older checkpoints below are historical; see FINISH-1.0.md for the current ledger.
## November 2 — administrator viewing controls
Removed app-authored Stop viewing / Allow viewing, Stop device screen sharing and the custom notification Stop sharing action. The visible indicator, Android consent/system revocation, password/lifecycle guards, server authorization and lease expiry remain unchanged. The existing native regression verifies an informational non-clickable indicator with no stop/allow buttons, continued JPEG frames after resume, background frame clearing, administrator session termination and fresh login.
Verification: 185 JVM tests (44 core, 56 mobileapp, 85 host), eight Node guard tests, lint zero errors/warnings (two existing informational hints), debug/QA/test APK builds, and the real TC22 session-monitor scenario passed; all 112 native requests matched published semantic operations. The disposable QA packages were removed. Whole-device Android consent was not rerun for this UI removal.
Installed the verified debug APK D8109C61E71660D2195337DA988F7F692CC4A0DBE95F61230930D6549CAB4D9B on the normal TC22 with install -r and no data clear. The process restart required sign-in; restored the existing scanner account and verified Connected, the Echo catalog entry and existing approval. Local server57832 remains HTTP200 and the print service remains Running. This APK also contains the previously verified optional raw-scan metadata client, gated off against the unchanged installed mobile1.4 server. Existing unrelated source/test changes are preserved. No commits, publication or broader release work resumed for this scoped request.
## November 2 — current checkpoint
Source candidate: immutable `ScannerCapture` retains SDK raw bytes as canonical Base64 before the callback returns and routes them through the ordinary semantic process payload. New optional fields are sent only after same-server discovery advertises mobile-scanners >=1.5,<2; older servers still receive the five legacy keys. Barcode text and symbology remain unchanged, scan/capture string representations are redacted, and a failed-submit regression proves the entire event is resent unchanged. Verification passes 185 JVM tests (44 core, 56 mobileapp, 85 host), lint with zero errors/warnings (two existing informational hints), debug/E2E APK builds and QA Android-test compilation. New native tests and operator installation have not run. The SDK configuration is unchanged and AIM extraction is not implemented by this transport slice.
Completed and verified:
- Android source `fb3cc5d` is signed and published as Gitea PR 10. It passes 174 JVM tests (44 core, 55 mobileapp, 75 host), lint with zero errors and zero warnings, and the debug APK build.
- The native `SameOriginInterceptor` fixes the cookie-bearing heartbeat that returned 403. The server's same-origin guard is not weakened.
- The normal TC22 installation was updated without clearing data. Existing login and device approval, heartbeat, the real print client's printer, fresh 4x1 media and published document selection, and Cancel then Leave all work against the one local server.
- That server is one QQQ demo on host port 57832. It runs Mobile Scanners/Echo, Collaboration/Record Files, Document Generation and Printing (API 1.10.0, mobile 1.4.0, printing 2.3.0). On this workstation, device port 8080 forwards to host port 57832. The original accounts, enrollment and approval are retained.
- The Printing owner has aligned the loaded 4x1 stock (101600 x 25400 microns at 300 dpi) and updated the real media revision. A real Echo 4x1 barcode document is published. The TC22 selected the actual printer, the fresh 4x1 media and that document, and Next succeeded and shows Scanner ready and Connected.
- The emulator is off because it stalls this workstation. Do not restart it automatically.
Not yet done:
- No physical Echo print or rescan has been proven. One separate synthetic HTTP Echo event reached the actual Windows spooler; physical paper and handheld print/rescan remain unconfirmed. One physical scan of QQQ-TEST-123456 has been requested from the user.
- Still open for 1.0: physical Code128 and leading-zero QR scan, print and rescan; full provider, camera, GPS, projection and device qualification; public coherent artifacts; primary Gitea CI; and final signing and distribution. Signing is deliberately last, by the user's decision. The first demo working does not collapse these requirements, and nothing here claims release readiness.
- Core native API merge continuation PR 1337 (a42f9e5) was reviewed with no actionable conflict regression and its 57 focused tests were verified. Normal CI and publication are not established, and default dependencies are not repinned.
The entries below are historical. Where they describe the printer media report, the 4x1 document or the same-origin fix as pending, this checkpoint supersedes them.
## November 2 — native cookie Origin compatibility verified on TC22
The combined Echo Demo on57832 (reached through device port8080) returned200 for login, then403 for the cookie-bearing session heartbeat POST, because Android's native QQQ requests omitted `Origin`. The server's same-origin guard remains enforced. Core now has a small `SameOriginInterceptor`: it sets `Origin` from the actual request URL's canonical scheme/host/port only (default ports omitted, IPv6 bracketed, never credentials/input/headers) and is installed last so additional headers cannot override it. It is used by `NetworkQQQRepository` and the `DeviceApi`, `MobileSessionApi` and `RecordFilesApi` clients. Version discovery, presentation/logo fetches, OIDC/browser/token requests and redirect refusal are unchanged.
All174 JVM tests passed (44core/55mobileapp/75host), with zero failures/errors/skips. Lint has zero errors/warnings and two existing informational hints; debug APK build passed. MockWebServer tests cover exact Origin plus cookie on real POSTs, canonicalization, override resistance and redirect non-forwarding. Session and record-file host tests assert the header on their actual requests.
The verified APK (SHA256 `0B872005B2FA84F693E73CC49133F5C1C02F732A034AA0635A2B50682A61A98E`) was installed on the normal TC22 without clearing its data. Existing scanner login and device approval work against the actual combined server; heartbeat remains connected. Echo opens its three required choices, the approved printer and its reported media load, and Cancel -> Leave returns to the app normally. This directly qualifies setup/session/cancel against the strict host, not a physical print. The earlier112-request/two-synthetic-scan acceptance remains separate. (Superseded: the printing owner has since reported the 4x1 media and the 4x1 document is published; see the current checkpoint above. Physical printing is still unproven.) Emulator stays off.
## November 1 — required Document choice verified on the real TC22
Echo asks for printer, paper/label size and Document once; all subsequent scans use those server-pinned choices. Android production code is unchanged: the generic semantic INTEGER/PVS/dependency controls handle it. The updated separate E2E app passed all7 stages on the actual TC22 (select Document, clear stale choices on parent change, validate required fields, queue two scans, recreate without duplication, Apps/Leave/CLOSED). All112 requests matched published semantic operations. Disposable app/reverse/fixture cleanup passed, normal app/data preserved, emulator off. Printing and scanner inputs are simulated; physicalScannerQualified/physicalPrintingQualified remain false.
Verification:99 focused/host JVM tests,8 Node guards, zero lint errors/warnings and both disposable APKs compiled; after a test-only fixed-footer click correction, the actual TC22 acceptance passed. Companion server source8da274e is signed/pushed with45 library and24 composed demo tests passing. Private live server53118 still has header fix101efc8 pending document-choice rollout; real printer pairing retry awaits the current Windows consent. Installed normal APK324227CC... remains sourcea2f0080, whose production code is identical to this test-only change. Full physical/provider/camera/GPS/projection/public artifact/signing gates remain open.
## November 1 — Echo native semantic choices verified on TC22
Claude implemented named process possible values using the actual published `searchProcessPossibleValues` JSON contract, dependent-field metadata, cancellation/error/retry and stale-response rejection. The unchanged Echo fixture now completes login, enrollment/approval, printer/media selection once, two accepted synthetic scans, rotation/recreation without duplicates, and Apps/Leave closing the run on the real TC22. All86observed requests matched published semantic operations. Disposable-package cleanup passed; normal package/data and real printers were preserved.
Validation:169JVMtests,8Nodeguards,236-file source standards, zero lint errors/warnings and bothE2EAPKs pass. This is simulated printing, not EMDK or physical output qualification. The remaining immediate blocker is the real Generation/Printing adapter and template/host composition, followed by physical print/rescan. Normal installed APK remains625a275; normal server API1.9/mobile1.1. Production signing remains deferred.
# Session state
## Echo semantic presentation source checkpoint
The optional extension 1.4 acknowledgmentLabel and manualEntryEnabled hints are published on feature/echo-scanner-presentation (draft PR9). Existing defaults remain Saved scan/manual enabled; Echo uses Print queued/manual disabled. Strict compilation, 167 JVM cases, lint zero errors/warnings and debug assembly pass. Hardware, lifecycle and receipt predicates are unchanged. The server companion passes 69 consumer cases plus two contract cases; the optional Echo module passes 15 strict QQQ/H2 tests.
The normal TC22 remains the separately installed 625a275 build (APK AF13778A…3E9216), preserving sign-in/data. Its About screen was opened and Query artwork, both public links and licenses verified through filtered accessibility data. New Echo-native UI/actual owner adapters/physical printing remain unqualified; printing and document/scripting owners have acknowledged their implementation lanes. The running normal server remains API1.9/mobile1.1. Production signing/distribution stays the owner's final step.
## November 1 standards fixes and About update
Applied the scoped standards review: copyright/SPDX, explicit imports, declaration headers, native official/three-space formatter profile, mechanical CI source gate and Kotlin warnings-as-errors. Strict compilation and163 JVM tests pass; all46 disposable sample native tests pass on TC22, and production QA instrumentation compiles. The native suite uses fixture providers; actual Android external-browser/provider acceptance remains separate. The existing formatter reports one unstable Gradle script and full formatter CI runner provisioning remains explicit in the review.
Settings → About QQQ Mobile now shows bundled Query artwork, app version, Dogfooding/Query public links and open-source licenses. The normal APK rebuild uses drawable-nodpi for the artwork. No credential/device parameters are added to website links. The preceding diagnostics/default-on GPS update remains installed until the preserving update recorded below; actual GPS fix is still open.
The real API1.10 Authentik desktop sequence now passes negative nonce rejection, A login, explicit company logout, old-cookie rejection, fresh B credentials/principal in the same retained browser context, and B local logout. Its provider-specific User Logout flow is required for company sign-out; default ordinary client logout stays local. Server0b53f34 is published and all8hosted checks pass. CorePR1369's exact architecture allow-list correction is being verified; full fresh core CI is not yet green.
## November 1 diagnostics and location update
Diagnostics now uses a full-screen, scrollable native view with a connection summary, local-time newest-first activity, All/Problems filters, pause/resume, copy/share/clear and Back. Existing footer and menu entry points still block process input while diagnostics is open. Location reporting defaults on for approved signed-in foreground sessions; Android grants remain required, explicit opt-out persists and automatic permission requests are not repeated after denial. Controls remain under Settings → Device permissions.
The combined client passes 159 JVM tests, zero-error/zero-warning debug lint, normal/E2E APK builds and the focused diagnostics interaction test on TC22 using the separate E2E package. The new UI/default policy still needs normal-package field observation. This does not establish a physical GPS fix, background tracking, real provider sign-out or production release readiness.
The preceding signed checkpoint1c766bf was installed in place as APK E34AC3C6…C1874EB with private backup, matching signer and no data clear. The operator and backend confirm one Code128 and one QR event on these bytes, including exact leading-zero value001234567890 from EMDK/QRCODE. Apps → menu → Scan Capture round trip was also confirmed. The normal server remains API1.9/mobile1.1; API1.10 core159 tests, current contract3 tests and coherent scanner/demo/files/browser78 cases pass separately while real Authentik acceptance continues.
## November 1 native authentication checkpoint
The optional company sign-out and original-nonce client changes pass 151 JVM tests, zero-error/zero-warning lint, debug assembly and native-test compilation. All 28 selected OAuth2 login/logout and authentication-lifecycle native tests also pass on the TC22 in the disposable sample application, with no failures or skips. The normal scanner package/data were preserved and its Activity is foreground again. These native tests use synthetic provider adapters; they do not establish real Authentik or external-browser acceptance.
Independent review covered all 26 implementation/test/documentation files in the frozen patch and found no security candidates (completed scan c358cde3-b51d-4451-b07d-8d1afe6c4485). This is a patch review, not a fresh audit of every unchanged file. Native OAuth now requires API1.10, the advertised nonce-validation feature and REQUIRED policy; TABLE authentication remains compatible. Company sign-out is separately advertised and explicitly selected; the automatic sign-out policy remains undecided.
The paired server changes and real A-to-company-logout-to-B acceptance remain in progress. Earlier real-provider testing established operator A and local QQQ invalidation, but retained browser SSO silently reused A; that failed handoff remains failed. This checkpoint is source publication, not a production release or installation of these bytes on the normal TC22 package. Signing/distribution remain the owner's final step. All historical entries below retain their original scope.
## November 1 status and visible QR fixture
All eleven hosted checks at portrait-default7de2b508 passed. Public QR fixture checkpoint2e0fd962 is signed and pushed; docs/TEST-BARCODES.md records independent ZXing decoding of001234567890. Physical QR acceptance is still pending. Normal TC22 APK C89161C…C6340 remains installed with portrait requested, and the backend now has nine trusted EMDK Code128 events (eight on prior F3AF and one after the portrait update).
Release1.0 remains incomplete: actual Authentik acceptance remains pending; the local TLS proxy correction is verified, and provider configuration returned a validation error under diagnosis; physical QR, camera/gallery, navigation/failure, projection and GPS acceptance remain open. Filesystem portability passes142 discovered/138 passed/four existing skips, and the separate RF HTTP consumer now passes18 cases on a transitional custom-filesystem tuple. This supersedes its earlier fixture-compilation failure; it does not establish immutable/default dependency adoption or change the normal API1.9/mobile1.1 runtime on57832. Signing/distribution remain last by owner instruction. Current summaries supersede dated open-state statements below without changing their historical evidence or sealed review scopes.
## November 1 portrait default installed; prior source fully green
The user requested portrait lock by default. Missing/invalid preferences now choose Portrait; all explicit saved choices remain available. Four focused policy tests, debug lint (zero errors/warnings) and debug assembly pass. Development APK C89161C…C6340 was installed in place after signer verification and a new private-data backup; no app data was cleared. Android reports the normal Activity resumed with SCREEN_ORIENTATION_PORTRAIT, and a direct screenshot confirms the signed-in Apps menu. Barcode/lifecycle acceptance of these new APK bytes remains pending; earlier F3AF physical evidence is preserved.
All eleven hosted checks at229fce3 passed, including both GitHub native runs (jobs124829276568/124828879619):35 shared cases, PixelCopy and all seven semantic scenarios. Logs show session-login-action-ready, session-fresh-login and the semantic request audit in both runs. Server documentation checkpoint2de8470 also passes all eight checks. Fresh checks will qualify the portrait-default source update. The operator confirmed the eight earlier Code128 events were deliberate trigger pulls. The QR was not visible in chat; a direct image link was supplied after unreliable browser automation, and current QR acceptance remains pending.
## November 1 physical EMDK positive control verified
The operator reported Saved scan on the normal F3AF development app. Direct ownership-guarded backend inspection verifies eight new Code128 records for QQQ-TEST-123456, source zebra-emdk, TC22 attribution and eight distinct event IDs. This closes the current trusted hardware positive-control gap for Code128 only. Operator confirmation of repeated trigger intent and the current leading-zero QR remain pending; [physical acceptance](PHYSICAL-TC22-ACCEPTANCE.md) records exact APK/runtime and remaining lifecycle/retry/ownership gates. No normal app/data clear or QA run occurred.
Strict Firefox profile-only CA trust now passes issuer/hostname checks using the supported PLAYWRIGHT_FIREFOX_POLICIES_JSON entry point. Windows trust is unchanged; the earlier Windows-root approval question is obsolete. Actual provider startup/exchange remains unexecuted while an independently reviewed known-listener guard is prepared for genuine TIME_WAIT rows. Independent architecture closure of Android229/server2de reports no additional concrete source blocker and maps A1-A11 controls/tests; runtime/deployment/dependency/signing limits remain. Record Files compatibility now has a concrete exact-revision filesystem dependency gate in a new private cache; no frozen0515/default/runtime adoption occurred.
## November 1 native CI — rotation passes, fresh-login test readiness under correction
Android5ba PR run41297344726 passed:35 shared native cases, PixelCopy and all seven semantic scenarios, including session-monitor. The duplicate push run41297340376 also passed rotation/recreation/mirroring/administrator termination, then timed out waiting for Username after clicking Log back in. Its request audit shows completed Android logout200 and no second Android manageSession. Static review confirms that the test awaited only the label, which also renders while the logout action is disabled. This supports a test-readiness race; the failure artifact does not record the button state at the click, so no production cause is asserted. The correction waits for a final signed-out message and enabled action, asserts enabled and clicks once; it retains all fresh-login/session assertions. The opt-in device-screen test uses the same readiness requirement. Fresh native execution remains required.
Independent release automation review of three exact5ba files found no actionable source finding; [scope and hashes](RELEASE-AUTOMATION-REVIEW.md) retain external-configuration limits. A separate API1.9/mobile1.2 backend-only smoke passed readiness and shutdown with private credentials removed. Its launcher initially failed closed on the Node shim PID; actual Node resolution corrected the helper, and the unsuccessful fixture also cleaned up. No TC22 QA test, ordinary-app cleanup or normal-server replacement occurred.
## November 1 TC22 walkthrough — reported disappearance investigated
After the operator reported that touching the handheld crashed or moved the app, direct ADB inspection found the normal scanner process alive and its Activity resumed/focused, with no crash-buffer entries. A direct screen capture showed the connected sign-in screen; the black desktop mirror was not reliable evidence of an app crash. Restored the existing scanner demo login using locally encrypted credentials, and a subsequent direct capture confirmed the Apps menu. Opened Scan Capture to resume the manual walkthrough. No production defect has been reproduced or fixed by this inspection; physical trigger/navigation acceptance remains pending. No QA instrumentation or app cleanup was launched over the operator's normal app.
## November 1 morning — normal app updated; native test correction pending hosted verification
The normal TC22 application is now updated in place to the reviewed development APK `F3AF638E…E9FAA`. Matching signing identity and a new owner-only private-data backup were verified; app data/enrollment were not cleared. Non-streaming installation completed and the normal process is running against the preserved API1.9/mobile1.1 demo. This supersedes older unchanged-normal-APK statements below. Operator login/navigation/scanning confirmation is pending; no production release is claimed.
Published `e04fdb2` passed CircleCI and CodeQL, but both GitHub native runs failed the same session test: it requested rotation then another Activity recreation without waiting for configuration convergence, and the barcode draft was empty. The test now waits for landscape/resumed configuration and asserts the draft after rotation and again after explicit recreation. This is a test synchronization correction awaiting native execution, not a demonstrated production draft fix. Physical barcode attempts separately hit a wrong expected value (operator confirmed the old test code), a transient focus assertion and an ADB timeout; none is a full hardware pass. Focus preconditions now wait briefly within the120second stage deadline, and install timeouts identify the operation.
The opt-in QA screen-consent harness, shared HTTP helpers and both QA/E2E test packages compile. Its early safety checks reject missing operator opt-in and both protected demo ports without device access. Actual consent/Home/Stop/expiry/end-session acceptance remains pending. The harness author stopped at the workspace agent-credit limit; root completed the compile corrections and continues directly. A real Authentik fixture is compiled with pinned images downloaded, but no services or OS trust changes have occurred; temporary Windows CA approval is still pending.
## November 1 — refined device-screen client and published paired server
The refined client passes137 JVM tests, all package/lint gates, fresh four-APK inspection and the TC22 ordinary-UID scan-ingress regression. Continuous image draining retains one latest buffer; repeated Share is blocked through service binding. Fresh independent follow-up review found no concrete issue. The source-pinned1.2 server `1041693` is published and passes39 server/six demo/all15 browser tests with strict195-JAR/2,441-class provenance in a cloned private Maven cache. Hosted client CI now targets that exact source. No actual projection consent/physical trigger is proved; the normal APK remains unchanged and its running demo remains qualified1.1. See [candidate scope and hashes](DEVICE-SCREEN-ACCEPTANCE.md).
## November 1 — initial consent-based device-screen checkpoint
The optional device-screen client and paired1.2 server pass initial compilation and automated gates:135 Android JVM tests, lint/all package builds,39 server and six real-HTTP demo tests. Parent and fresh independent static review found no reportable client issue across the nine changed-file inventory; actual consent and projection behavior remain unverified. See [scope, hashes and remaining acceptance](DEVICE-SCREEN-ACCEPTANCE.md). The normal operator APK and qualified1.1 demo remain separate. Frame freshness/binding refinements and final native acceptance are still required.
## November 1 — hosted Back focus failure and physical-trigger checkpoint
Published Android head `6845abc5289fa975e6f50b23b159c3131a82eee1` has a successful CodeQL check; not all CI is green. CircleCI android-test job 140 fails three NavigationTests before injecting Back: their Activity is resumed and the device is interactive/unlocked, but Activity/decor window focus stays false for five seconds. Both job attempts reproduce the failure. Retained JUnit contains no focused-window owner, so the cause is not yet established. Failure-only diagnostics now classify the focused window as own/system/other, report bounded own-window flags and the known network-dialog visibility; they retain the five-second focus requirement and actual Back injection. CircleCI unit job 141 passed. One GitHub native run failed and the other was still running at inspection; neither is claimed successful.
The physical QA attempt using APK hash prefix `CFFF` reached its first hardware-ready stage but recorded no acknowledged operator scan. A final near-deadline HTTP poll timed out; this is not physical scanner acceptance. Evidence is preserved under `C:/Users/james/.cache/qqq-tc22-physical-attempt-20261101-0210`; `physicalTriggerVerified` remains false. A separate, unqualified test follow-up is prepared to avoid sub-second status polls and label deadline failures without extending the 120-second stage bound. Its compilation is deferred until the concurrent scanner changes are ready; it is not part of this diagnostic checkpoint.
The earlier normal-server ownership hold is resolved. After correcting the CIM positive control, the owner completed the reviewed upgrade to the frozen `ecf`/`b14` API 1.9 / mobile-scanners 1.1 TABLE runtime. A fresh backup, original-column invariants, old-after-new recovery logins before and after stopping the predecessor, and live login/metadata/logout for both demo accounts passed. Two devices and three scans, including the leading-zero value, were preserved. Private evidence is retained in `C:/Users/james/.cache/qqq-normal-api19-002594902b1942bda2480c512ed644ab/normal-verification.json`. The normal operator APK remains unchanged. Further optional screen-capture source is under separate implementation; these diagnostic edits do not qualify it.
## October 31 — published client checkpoint and probe target clarity
Signed client checkpoint `d316567` is published to existing draftPR8 and the clean canonical feature checkout is fast-forwarded to it. Fresh hosted checks found two CodeQL intent-redirection annotations in the separate permissionless test probe. Inline static triage at that exact revision classified both as not actionable: the exact QA/E2E package allowlist dominates both fixed-action broadcast sends, no arbitrary activity/service is launched, and the probe adds no sender authority. No alert was dismissed or completed audit rewritten. The probe now maps accepted requests to explicit final package literals rather than forwarding even the validated caller string. Probe-only build/lint and repeated ordinary-UID eight-forgery/manual-positive acceptance pass; production APK hashes are unchanged. Publish this source clarity follow-up and inspect fresh CodeQL/client checks before release. Physical trigger and whole-handheld viewing scope questions remain pending.
## October 31 — final SDK/navigation candidate verified for publication
All three Android Back handlers and the toolbar arrow now use one live Main-owned drawer-first action, preserving standalone fallbacks. The native regression first proved a real process-level bypass opening Leave behind the drawer; the corrected case passes with one actual Android Back, preserved process/draft and zero cancellations. It also covers seven other overlay/focus gates. Current source passes127 JVM tests,34 TC22 shared cases in115.717s and the additional overlay case in17.948s. A distinct-signer ordinary-UID test passes against the exact current QA APK: eight forged broadcasts cause zero extra process steps/records, while a manual positive control saves through the real API. Actual physical trigger proof is still false/pending; the barcode-visibility question is unanswered.
All four APKs pass fresh DEX, manifest, native-hash and packaged-notice inspection. Independent review of the final SDK/navigation patch found no concrete defects requiring changes. See [exact hashes and evidence boundaries](TRUSTED-SCANNER-ACCEPTANCE.md). The final code is ready for an explicitly authorized signed source checkpoint; hosted client checks must run after publication. Coreecf has18 hosted successes and published serverbf174a6 has8. New whole-handheld viewing scope clarification is pending; current capture is QQQ Mobile only. Normal operator app/server and emulator remain unchanged by these tests.
## October 31 — coherent backend accepted and published; final Android gate running
The owner explicitly authorized signed commits and publication after automatic review required direct commit approval. Core `ecf0f550` is published to draftPR1334. Server code `b14d925` plus acceptance documentation `bf174a6` is published to draftPR6. Fresh37 server/five demo tests, strict runtime provenance (195 JARs/2,440 unique QQQ classes/zero duplicates, Guava33.7.2-jre/H2 2.2.224), actual API1.9/mobile1.1 discovery and all15 browser tests pass. The normal operator runtime remains unchanged. Hosted checks are running; one Windows job failed to download PSScriptAnalyzer before project analysis and received one scoped retry.
The trusted scanner now also blocks physical input behind drawer/Settings/Diagnostics/Account/leave overlays and lost window focus, with a live callback guard. Two new coordinator tests and one production-widget QA native regression are added. Auth0 stable3.0.0 and Coil3.0.4 replace inherited prereleases; two native raster/SVG/error rendering cases are added. These source changes require the new combined build and device run. The first build correctly rejected stale notices: AboutLibraries10.9.2 did not declare its dependency graph as an incremental input. A lazy resolved-coordinate input now invalidates its standard generator; strict license/version gates remain enabled. The combined gate is running again. Earlier APK hashes and hardware evidence do not qualify these changes.
## October 31 — trusted scanning packages verified; runtime provenance caught a mixed dependency
The approved optional EMDK implementation passes all 49 scanner JVM tests, debug/release lint, debug/unsigned-release/E2E assembly, and production-path QA/probe assembly. The separately signed adversarial probe has a verified distinct test certificate; its temporary private key was deleted. These results do not establish physical trigger or injection resistance: disposable TC22 acceptance is next, and the normal operator APK remains unchanged.
Exact-source core `ecf0f550a0357cec6387c2b54efca128a405a629` passes the full selected dependency closure: 4,507 tests discovered, zero failures/errors, 16 existing skips, plus the configured coverage/static gates. Scanner server `a8ed108` passes 37 tests and its demo passes five. Runtime provenance then correctly rejected an old RC.20 backend dependency in the demo classpath. Browser and QA fixture execution are held while the consumer dependency management is corrected; these test passes are not qualification of that mixed runtime. The verifier remains strict, and no shared artifact or normal server was replaced.
## October 31 final client audit and isolated integration follow-up
The owner explicitly accepted Zebra's EMDK terms and selected native trusted scanning. The optional adapter is now being implemented; the earlier 32-test result does not qualify that new hardware path. Wrapper checkpoint `7e0cbee` and the five verified client security fixes `df532ff` are signed local commits, not yet pushed. Core's local `331a99d` additionally pins patched Guava; its full selected dependency-closure suite and verification analyses are running in a fresh private repository. On Windows, only the inspected cosmetic shell coverage-summary execution is omitted because of quoting; actual coverage checks and static analyses remain enabled. Hosted Ubuntu retains that summary.
The completed remediation batch passes 109 JVM tests, scanner lint with zero errors/warnings, debug/native-test and unsigned sample release assembly. All 32 selected TC22 native cases pass in 117.325 seconds, including ten new authentication lifecycle tests. Actual packaged sample release controls deny cleartext and backup and include all ten cloud/transfer exclusions. Five audit fixes have regression evidence; trusted scan ingress remains open pending the separate EMDK license decision. The new debug APK `8c4d3886...c512a` is not installed over the operator app. See [the scoped review checkpoint](SECURITY-REVIEW-2026-10-31.md).
The deep client audit completed and sealed against clean published `d1de0bf`, with one medium and five low findings. The medium finding is delayed password-session adoption after logout/server switch, including retained original-server models. Low findings cover sample release cleartext, failed Auth0 local sign-out, unauthenticated exported scan input, stale legacy Auth0 callbacks, and sample session backup. Source fixes are being prepared with delayed-response/provider and host regression tests; none is closed merely from editing source. Static coverage is partial, with external provider/server/dependency implementations and bundled binary implementation gaps excluded. Real configured Authentik and physical operator acceptance remain separate.
An isolated `fix/hosted-native-ci` checkout keeps this work separate from the audited source and installed operator app. Core draft PR #1334 publishes reviewed native API 1.9, credential-safe logging and explicit-null parser fixes at `00f6c49ef4c21bedb7c62ccad535bd6de1008948`. Focused source gates passed 154 tests with two pre-existing disabled cases, plus a separate 17-test null/HTTP gate. Server `a8ed108` canonical UTC migration passes 37 root and five demo tests and every hosted check.
A fresh private Maven repository and unique CI-only core version are qualifying the fixture from those exact commits; no shared artifacts or operator runtime are replaced. Hosted acceptance must include the nullable admin form and session-monitor scenarios. A failure-only focus diagnostic preserves the five-second guard and actual Back input; the cause of the hosted focus loss remains unproven.
## October 31 unconfirmed scan exit correction
The operator reported Leave process permanently disabled with “The last scan is not confirmed.” The existing JVM harness reproduced that settled missing receipts remained a hard navigation block. A new regression failed before the correction. Settled unconfirmed scans now show an explicit warning that they may already be saved; Stay retains the original retry event, while Leave process uses the existing server cancellation and clears the event after cancellation succeeds. Failed cancellation retains the event and exposes a separate, explicit Leave anyway choice that clears local state without another request; the dialog warns that the server process may still run and saved changes are not undone. Active requests and pending photos remain hard blockers, and receipt validation is unchanged. Added JVM guards/cancellation/fallback cases and a native lost-response/malformed-receipt/Stay/failed-cancellation/explicit-exit regression. Final bounded verification passed all 46 mobile JVM tests, scanner lint/debug APK and sample/test APK assembly in 18 seconds. The TC22 follow-up passed all 21 selected native cases in 53.482 seconds, including the new lost-response/failed-cancellation/explicit-exit case. The operator update is being coordinated separately; actual configured Authentik acceptance remains pending.
## October 31 navigation/authentication review checkpoint
The combined client checkpoint passed 102 JVM tests (26 core, 43 mobileapp, 33 scanner), scanner lint, debug/E2E APK assembly and native-test compilation. The final follow-up build passed in 1m3s (266 tasks, 34 executed). The TC22 rerun passed all 20 selected navigation/input/account/OAuth tests in 49.235 seconds, including all seven synthetic AppAuth cases. The first run exposed Back during drawer opening; the handler now also handles a drawer targeting Open, and the native helper waits for settled UI before hardware Back. [Native acceptance evidence](NATIVE-AUTH-ACCEPTANCE.md) distinguishes real handheld execution from the simulated browser/provider. No actual Authentik sign-in is claimed. New hosted CI is required after publication.
Independent source review found no concrete defect in the native authentication/navigation patch; this is not final security or deployment clearance. [AppAuth package evidence](third-party/APPAUTH-VERIFICATION.md) confirms 131 bundled entries, 126 external runtime dependencies, nine complete texts and unchanged eight native libraries in the inspected debug/E2E APKs. OAuth2 depends on the companion core UI API1.9 native-session contract, still pending publication/host adoption. TABLE_BASED remains usable on compatible API1.8 servers. Production signing/distribution and actual shared-operator/provider acceptance remain separate gates.
## October 31 semantic authentication — synthetic native acceptance passed
The combined source passes 102 JVM tests (core26/mobile43/scanner33), scanner lint and debug/E2E/test APK assembly. All seven synthetic native OAuth cases and the navigation/input/account suite passed on TC22 as described above. Generated notices contain 131 entries including AppAuth. The initial sandbox run stopped at the existing debug-key lock; scoped rerun preserved the key and completed successfully. Testing used the separate sample package; no emulator or normal operator installation was started for this checkpoint.
The native descriptor now requires an advertised `prompt: "login"` policy, applied through AppAuth and bound to each pending attempt. Missing/unsupported prompt policies fail closed. Focused tests cover policy changes and mismatched returned requests; the real shared-handheld acceptance gate includes switching operators with an existing browser SSO session. This requests fresh authentication; it is not global IdP logout or verified `auth_time` proof.
The owner requires Authentik when configured by the server. Generic OAuth2 browser sign-in is implemented against an additive UI API1.9 `nativeSessionLogin` capability, with explicit issuer/discovery/callback metadata and an `authenticationFlow` marker on the existing session exchange. AppAuth performs authorization with S256; the secret and code exchange stay on QQQ. TABLE_BASED remains unchanged and native bearer metadata is kept distinct. Client gates, callback/lifecycle checks, deferred cookie adoption and focused tests are implemented; actual configured Authentik acceptance remains pending. See [AUTHENTICATION.md](AUTHENTICATION.md). No live provider settings or operator data were changed.
## October 31 navigation correction — native acceptance passed
The primary navigation uses one hamburger drawer with All apps and server-authorized app entries, plus Settings and Diagnostics fixed at its bottom. The duplicate toolbar Apps button and Switch app overflow action are removed. Back remains beside the hamburger within apps/processes, Account stays separate, and prelogin/pending-approval drawers hide business destinations. Drawer dismissal and secondary surfaces preserve active work; selecting a primary destination retains the existing leave and pending-upload guards. Compilation and the selected native navigation suite passed, including the drawer-opening Back correction above. The successful checkpoint below describes earlier session/photo acceptance. No installed operator app, server or data was changed for this correction.
## Current verified checkpoint — October 31
All 12 shared TC22 navigation/input/account tests pass. TC22 orientation (all five persisted policies), offline licenses, synthetic camera/gallery/rotation/remote-logout cleanup, and the full session-monitor scenario pass. Session monitoring includes real JPEG app pixels, background clearing, renewed viewing on resume, operator pause, administrator termination and fresh login. Photo/session request audits match published semantic operations. JVM reports total 91 tests (22 core, 36 mobileapp, 33 scanner), zero failures; scanner lint passes. Notice coverage gates and packaged APK inspection pass for debug/release/E2E (125 runtime dependencies, 130 entries and nine complete license texts), and negative omission/blank-text cases fail as required with generated metadata restored.
The false-offline investigation was a test-clock issue, not a demonstrated timezone defect. The independent standalone file-backed JVM test in America/Chicago passed across 18 seconds. No timezone workaround or relaxed heartbeat/offline policy was added. Remaining work: publish the Android review checkpoint and verify fresh hosted CI, finish independent full audits/native attribution, qualify immutable optional dependencies/CI and real lens/operator acceptance, then return to signing/distribution as the owner requested. The normal installed app and operator demo remain on their earlier versions; isolated test acceptance is not deployment. Historical entries below preserve earlier failures and are superseded by this checkpoint.
Viewing diagnosis resolved the apparent offline state: the test advanced only16ms of Compose coroutine time per250ms wall polling, so heartbeat LaunchedEffect timers did not run during125 widget polls. The standalone non-UTC file-JVM regression passed and audit ordering confirmed zero heartbeats during those polls. The helper now advances by elapsed monotonic wall time. The next TC22 run reached `session-mirror-frame`: actual app JPEG pixels were delivered/decoded and background clearing passed. It then failed the already-identified old resume assertion because no viewer demand was renewed. Resume now polls the admin widget again; rebuild/full scenario rerun is active in the current turn. No production heartbeat interval or offline threshold was relaxed. The standalone regression source is in the companion demo, added by the read-only diagnosis agent and verified separately.
Latest viewing diagnostics: eligibility is available=true, but no indicator/frame upload occurs; 125 widget requests and 18 native heartbeats succeed. A real HTTP regression now explicitly requests the viewer widget as a browser (without the Android client header) before the native heartbeat; both demo tests PASS, ruling out the simple cross-channel lease-isolation hypothesis in that fixture. Actual core widget response fields are `layout`, `blocks`, `type`, `blockTypeName`; corrected the diagnostic extractor to read `blocks` (the earlier blank status was an incorrect `subBlocks` assumption). That diagnostic change needs rebuild/rerun to obtain the actual server widget status. All runs in this checkpoint are terminal. No production state-sharing change was made on an unproven hypothesis.
The window-focus synchronization rerun advanced past capture eligibility: no APP_NOT_FOCUSED failure remains in that run. It now times out at the viewer indicator. Request audit shows 125 successful web widget renders and 18 successful Android heartbeats, with no screen-upload operation. Added bounded failure diagnostics for current server availability/reason and the widget's status text (never image data or credentials), to distinguish lease issuance from client rendering. This newest diagnostic needs rebuild/rerun. All native processes from that run are terminal. Android changes are staged as a review checkpoint, but later focus/diagnostic edits remain unstaged; re-stage and verify before committing.
Screen-viewing follow-up reproduced a stale tracked-focus flag: server reported APP_NOT_FOCUSED while instrumentation confirmed interactive=true, no keyguard/device lock, both Activity/decor focus=true and lifecycle RESUMED. ScannerActivity now observes actual view-tree window-focus events, resynchronizes on resume, and removes the observer on destruction. Existing captureAppScreen independently checks actual decor focus, so capture protection remains enforced. Scanner unit/lint/E2E APK verification passed (build1m19s); the focused real-server TC22 scenario is rerunning in the active root turn. The operator demo remains on extension1.0; no normal-app update was made.
TC22 photo acceptance PASSES against the verified guarded0352 fixture: synthetic external-camera result automatically uploads, exact hash/size/provenance/parent and collection association are inspected, gallery staging survives rotation/recreation, leaving is blocked while pending, and remote session termination clears the private copied original without deleting the selected gallery source. All recorded native requests match published semantic operations. The run exposed and fixed a real UI-thread defect: ScannerActivity now dispatches unauthorized-session navigation on the main thread and ignores callbacks for a superseded session. Scanner unit/lint/APK rebuild passed before the successful native rerun. Fixture owner metadata remains in the private task cache; do not infer a normal-app deployment or actual lens capture from this synthetic result. Real session-viewing acceptance is starting next in the active root turn.
TC22 navigation/input/account acceptance now PASSES all 12 tests on rebuilt sample APKs. Orientation (all five policies) and offline licenses also pass. The guarded Record Files private launcher needed execution outside the sandbox for its process to survive; current private fixture PID111928 was started with a 900-second lifetime, with protected metadata path in the task cache. The first native photo run reached Saved and verified exact bytes/provenance/parent, then failed because the test assumed stored collection order was zero-based. Actual owner storage is one-based while manifest/receipt order is zero-based. Corrected that assertion and the duplicate Apps selector; photo rebuild exec64104 is active in the current turn, then rerun the synthetic camera/gallery/rotation/remote-logout scenario. Preserve fixture ownership and stop only that verified fixture when finished. Session mirroring acceptance and final audits remain open.
Latest navigation rerun is terminal: 11 of 12 TC22 tests pass. Restored account/server assertions now pass. The remaining failure selected both the menu action and the closed drawer heading named Switch app; a stable menu-action identifier and matching test selector are added. Those last two changes need rebuild/rerun. No native test or fixture remains active from this run.
TC22 follow-up: orientation and offline licenses each PASS their instrumentation scenario. Shared navigation ran 12 tests: 10 passed, with failures from duplicate visible/closed-drawer Apps selectors and an old account-dialog expectation that server selection still lived there. Added a stable toolbar Apps identifier, preserved the account user assertion, and verified restored server selection under Settings instead. Rebuilt sample/test APKs successfully; navigation rerun is starting in the current root turn. Guarded server changes and passing 18-test evidence are committed/pushed as84ffd7c on the existing draft PR; no merge or production release.
Latest TC22 result: the rebuilt orientation acceptance PASSED (one instrumentation test covering all five policies, persisted selection, actual fixed screen axis, Activity recreation and a fresh Activity launch). The bounded configuration-convergence wait resolved the prior timing failure; the axis assertion remains enforced. Guarded Record Files integration now passes all 18 tests and runtime/Checkstyle verification, with evidence retained in the companion module. Root is running TC22 licenses followed by shared navigation acceptance as exec session85018; poll that handle before starting another device run. No operator-app update or production release is implied by isolated test acceptance.
Latest verification follow-up: the notice-integrated APK build log ends BUILD SUCCESSFUL. Explicit TC22 orientation acceptance ran and failed because the observed screen axis was still portrait when landscape was requested. A bounded 15-second configuration convergence wait was added to the native test while retaining its final axis assertion; rebuild/rerun remains required. The disposable test runner cleaned its resources. Emulator remains closed. Record Files guarded-artifact verification has four unresolved intake-event concurrency test failures; see the companion integration session state. No production release is qualified.
2026-10-31 Android acceptance follow-up: the bounded pre-notices build passed 89 JVM tests (core22/mobile36/scanner31), normal and isolated APK assembly, and scanner lint with zero errors/warnings; broader modules retain inherited vector/dependency warnings. The emulator remains off after confirmed workstation stutter. On TC22, session capture remained safely blocked with APP_NOT_FOCUSED after recreation; no mirror frame was uploaded. The 12 shared navigation/input/account tests then ran: six passed and six failed (stale account/server-confirmation assertions, duplicate catalog/drawer text selectors, and a hardware Back case). Assertion corrections and window-focus diagnostics are prepared; these failures are not counted as passes. Orientation Settings, retained manual draft state, synthetic photo E2E, explicit physical photo fixture support, and bundled-license Settings entry are now implemented but require the next compilation/native acceptance. The license owner is running the bounded integration qualification. Ordinary scanner app/data, DataWedge settings and existing USB reverses were preserved.
## 2026-10-31 — Navigation hierarchy and release preparation
Owner explicitly deferred signing/distribution to the final step until the channel is chosen. No signing key or production signing configuration has been created. Continue implementation, audits, native/physical acceptance, documentation and ordinary CI; do not create release tags or force a distribution decision early.
Workstation interruption: user reports whole-desktop pauses whenever the emulator acts and explicitly confirmed the workstation is smooth after closing it. All local emulator/build runs were paused and emulator-5554 shut down gracefully without clearing application data. TC22 remains connected. Sampled host had 48 logical CPUs, CPU17%, about152GiB free RAM of256GiB, GPU10% with6.1GiB/30GiB used; these samples do not rule out brief spikes. WHPX is operational outside the sandbox. The running host-GPU emulator log includes OpenGL context preemption/restore failure and external-memory import errors; graphics interoperability is a hypothesis, not a proven root cause. Saved AVD config now uses software graphics, previous config backed up locally, but no comparison run has been made. Do not restart the emulator automatically. Continue acceptance on TC22/hosted CI or schedule a controlled software-renderer comparison later.
Android checkpoint: primary Apps, an app-switching drawer, separate Account and Settings, host device/location settings and shared leave confirmation are implemented. A bounded build with the emulator closed passed 85 JVM tests (core22/mobile35/scanner28), scanner lint with zero errors/warnings, isolated scanner APK/test APK and shared native-test compilation. The focused native long-form test passed after waiting for actual IME dismissal; the earlier immediate assertion raced its animation. Expanded native navigation cases are compiled but not run. Photo capture/picker, registered Record Files transport, stable retry identity, exact receipts and lifecycle tests are implemented. Subsequent camera auto-upload/destruction cleanup, the public mobile-record-files vocabulary gate and associated tests await the next bounded build after concurrent asset/backend work finishes. No emulator restart is authorized; physical acceptance uses the separate explicit runner in [DEVICE-TESTING.md](DEVICE-TESTING.md).
The session native test now passes login/approval/catalog/process and recreation, including parent-app metadata and denied-location reporting, against the corrected file-H2 server. It still timed out before the administrator-viewing indicator. The test now waits for explicit screen eligibility and renews the widget viewer lease like the real admin poller; that change is unverified. Actual PixelCopy pixel-boundary instrumentation passed earlier, but full session/mirror/admin-end native acceptance is not complete. Ordinary synchronous process responses have no generic status endpoint: the host retains the process reference, prevents replay and describes uncertainty before explicit cancellation; it does not promise unsupported result checking.
Current plan: finish native navigation/session acceptance; integrate camera/gallery with the actual Record Files runtime; freeze paired revisions for final independent reviews; qualify signed delivery and hosted CI before publication. Apps → app → process is primary navigation. Account/sign-out, Settings and diagnostics are secondary surfaces that preserve active work. Toolbar and Android Back share one guarded leave policy. See [NAVIGATION-UX.md](NAVIGATION-UX.md); implementation and native acceptance are underway, not yet complete.
Latest intermediate results: Android 76 JVM tests and lint with zero errors/warnings; shared native 11/12, with the remaining form/IME assertion under investigation. Companion server 37 tests, two real HTTP acceptance tests and all 15 browser E2E tests pass. The H2 last-connection close/compaction issue was reproduced and fixed with the demo database lifetime setting; the browser suite now completes in 59.4 seconds. These counts describe their tested intermediate source, not a final release.
Record Files now has actual registered ingestion routes and a frozen 19-file consumer bundle (`retained-contract-20261031-0238`), all hashes verified by the optional integration owner. Camera/gallery implementation will use its existing storage and durable receipts. Atomic authorization across revocation and upload commit remains a shared contract issue; runtime compatibility and native acceptance are still being qualified.
Release preparation adds optional environment-only signing, validated version properties and a separate scanner-tag workflow that calls the native verification lane before building/signing/publishing. Actionlint passes. The signing identity/secrets, actual signed build/upgrade and final publication have not been configured or verified. See [RELEASING.md](RELEASING.md). Debug/E2E defaults and the operator's installed TC22 application are preserved.
## 2026-10-30 — Sessions, screen viewing and audit corrections (uncommitted)
The host now requires mobile-scanners `>=1.1.0 <2.0.0` and uses registered heartbeat/end/screen operations. Session telemetry contains screen metadata only. Administrator viewing leases enable bounded own-window PixelCopy JPEGs with a visible operator stop control. Capture blocks login/password screens, unrendered transitions, background/unfocused windows and ended sessions; checks repeat after capture and before upload. Foreground location is optional and permission denial leaves scanning usable. Device approval displays the server-generated short name and supports administrator renaming through QQQ.
Audit fixes require complete scanner receipts and reject invalid explicit maxLength. Navigation now follows NavController's actual destination, including system Back and controller replacement. Shared renderer/authentication fixes are being completed by the parallel review agent. Current intermediate verification: 75 JVM tests passed, APK builds passed, lint zero errors/eight warnings before dependency catalog cleanup. Actual PixelCopy instrumentation passed after an emulator startup ANR was resolved by a cold restart preserving data. Shared native suite passed 11 of 12 tests; a new short-viewport login regression found the IME obscuring the button and its fix is awaiting rerun. Both new navigation tests passed. Real-server session/end/viewing scenarios are implemented but await the rebuilt companion server and final native run. These are work-in-progress results, not publication or production acceptance.
At this October 30 checkpoint, Record Files camera/gallery routes were unavailable. The October 31 checkpoint above supersedes that dependency status. Version one remains online only.
## 2026-10-30 — Semantic API migration in progress
The current work makes the scanner host use the registered semantic UI API contract for every client operation. Public `/qqq/v1/version` discovery gates credentials, branding and metadata; the supported range is core `>=1.8.0 <2.0.0` and mobile-scanners `>=1.0.0 <2.0.0`, with `android` advertised in `clientChannels`. The four mobile extension operations now resolve under `/qqq/v1/qbit/mobile-scanners`. The native renderer applies server-directed completion, field/step updates, form submission and cancellation, and explicitly blocks unsupported required controls. Review fixes honor hidden fields and explicit null clearing, and keep generic fields named `eventId` separate from scanner retry context. See [UI-API.md](UI-API.md) for the exact rendering boundary; this is not a claim of full web-client parity.
The final combined JVM suites passed 53 tests: core 21, mobileapp 23 and scanner-host 9. Lint passed with zero errors and six warnings; normal debug and isolated E2E/test APKs assembled. A native administration regression exposed that core QQQ completes a process by omitting nextStep from its COMPLETE response. The renderer now honors that server signal, preserves the final display step until submitted, and closes completed processes without issuing cancellation; a ViewModel regression verifies the full sequence. The companion qbit passed root verification/install with 23 tests and demo HTTP verification; all six migrated browser E2E scenarios passed in 2.9 minutes. The first native run exposed HTTP400 because the server had not registered the Android client channel. The server now registers it while preserving existing channel policies, and the real HTTP regression sends the Android header on all requests and passes. All six native semantic scenarios passed across focused runs: incompatible core major, missing extension, missing Android channel, supported-server scan, additive-future metadata scan, and native administrative approve/revoke/cancel. The first five passed before the terminal-state correction; the final administration scenario passed afterward in 255.917 seconds. Its OpenAPI audit matched all 31 requests to 13 published operations, including backend cancellation. Safe stage markers show both processes reaching completion and Close, then a populated form being cancelled without changing persisted status. The emulator was CPU-bound with animations enabled; the runner now wakes/unlocks only the selected emulator, temporarily disables its animation scales and restores them during cleanup, allows 60 seconds for UI conditions and 300 seconds for instrumentation. The six shared native sample tests also passed. Hosted Android verification remains pending; earlier records below apply to their earlier revisions.
Active plan: publish the Android branch and verify hosted results. The companion server is published at 7dd02b8874882ca2edcdcd89109b3dee6b4b0c88 with all server CI checks green, and the Android workflow pins that revision. The six shared native sample instrumentation tests passed on API35. The local server and emulator restarted after the workstation crash with data preserved.
Physical acceptance now includes the Zebra TC22 on Android 14 with DataWedge 13.0.325. The latest normal APK was installed with login/enrollment preserved; browser record approval displayed one device approved. The native catalog and Scan Capture opened with Connected status. The DataWedge profile associates all scanner-app activities, enables barcode input, disables keystroke output, and sends the SCAN action/default category as a broadcast to the explicit package with signature checking enabled. The user performed two deliberate physical triggers and saw Saved scan. The matching persisted Code128 row was independently inspected in the backend: synthetic barcode QQQ-TEST-123456, symbology LABEL-TYPE-CODE128, source scanner, user scanner, TC22/Android14/app0.1.0, mobileScanApp/mobileScanCapture, and capture/receipt timestamps. The two deliberate Code128 triggers produced two records, one event per trigger. A subsequent physical QR scan also displayed Saved scan; the independently inspected row preserved the leading-zero barcode 001234567890, symbology LABEL-TYPE-QRCODE, scanner source and the same TC22/user/app/process attribution with capture/receipt timestamps. The table contained exactly three records for two Code128 triggers and one QR trigger. Connection-loss/retry and revoked-device behavior still need physical-device acceptance; emulator checks do not replace those cases.
## Earlier verification records
2026-10-30 process E2E follow-up: added a real scanner-host Compose instrumentation scenario against a fresh QQQ demo database. It drives server selection and login, confirms pending enrollment, approves through the real admin process, selects the native app/process, submits a leading-zero manual barcode, waits for the actual saved-scan acknowledgment, independently queries durable provenance, and verifies a revoked device cannot save through its already-open process. The final scenario passed on API35 (one end-to-end test, 50.932 seconds), including completion of administrative result screens. It uses the separate `io.qrun.mobile.scanner.e2e` application; the operator's app, accounts, data and existing forwarding are preserved. Random test credentials are passed through stdin to private app storage and redacted from reports. The runner removes both test APKs and cleans up its temporary server/database and dedicated forwarding; only the normal scanner package remained installed after verification. Added a pinned companion-server checkout and this E2E to the push/PR emulator lane; hosted execution remains unverified until publication. Final required Gradle verification passed: 28 JVM tests, lint zero errors/six existing warnings, normal debug APK and isolated E2E/test APKs assembled. Actionlint, Node syntax validation and diff whitespace checks passed.
2026-10-30 follow-up: the scanner could show a blocking “No Network Connection” dialog while its server footer correctly said Connected. The shared legacy callback used one Boolean for multiple networks; losing either network incorrectly marked all connectivity unavailable. It now tracks network handles, with two regression tests that fail against the old logic. Scanner hosts opt out of the legacy internet-presence modal and use the existing server reachability footer; server authentication and device approval are unchanged. All 28 JVM tests and six native API35 instrumentation tests passed, lint has zero errors/six warnings, and the replacement APK was installed without clearing app data. Scanner activity and its existing session were restored after tests.
The local AVD is configured for host NVIDIA graphics (`hw.gpu.enabled=yes`, `hw.gpu.mode=host`), with the previous config backed up under the user's cache. Reopening with `-gpu host -no-snapshot` and restoring adb reverse showed Home frame percentiles of 16/17/18 ms (50th/90th/99th) and scanner 19/22/48 ms. These are post-restart observations, not a controlled before/after benchmark; the original slow process had already exited. The actual previous renderer had used the GPU despite the saved AVD setting, so disabled GPU is not established as the root cause. The admin browser is open at the demo login on port57832.
2026-10-29 verification: all 26 JVM tests passed, lint reported zero errors/six warnings, and the debug APK assembled successfully. Runtime diagnostics now include sanitized native API and device-request outcomes, in addition to branding and heartbeat events. Android 15 system-bar insets were corrected after emulator inspection. The shared mobile library now declares its connectivity permission for all consumers. Login copy uses a neutral title beneath the customer branding. CI APK artifacts include their own `SHA256SUMS`; local builds can be hashed with `Get-FileHash`.
Repository implementation is published for review in [PR #8](https://github.com/QRun-IO/qqq-android/pull/8); it has not been merged or released. The branding/diagnostics commit `a5621c5` passed all hosted CircleCI, scanner build and CodeQL checks. The optional manually dispatched hosted emulator lane was skipped; local native emulator tests passed. The companion qbit is published at https://github.com/QRun-IO/qbit-mobile-scanners, with all hosted server/CodeQL/demo checks passing and protected branches/security settings enabled.
Final-APK acceptance passed: six native UI tests, safe Android 15 header insets, customer branding, neutral login text, diagnostic panel and Connected → Disconnected → Connected recovery. APK SHA256: `A25451EA7A1360474428F1B806134DDFD059190FB99E81B4F69E720808C3209C`. The visible API35 emulator is left at login with `http://127.0.0.1:8080/` forwarded to host demo port57832. No credentials were entered or scans submitted through the emulator; the full operator/approval/capture walkthrough and physical TC22 remain pending. The server HTTP acceptance suite separately verifies the complete authorization/capture flow.
Earlier implementation records follow; the publication and emulator statements above supersede their original pending status.
Runtime branding and diagnostics added: public server metadata supplies display name, primary color, and an optional HTTPS logo; malformed/missing metadata uses QQQ defaults and server changes reset presentation. A persistent foreground connection footer checks the public status endpoint, with bounded credential-free diagnostics Copy/Clear. Shared top-bar colors now use the matching Material foreground role and its connectivity callback is disposed correctly. Branding/status protocol, isolation, malformed input, response redaction, and log bounds have JVM regression tests. Validation passed: full 23 JVM tests (core12, mobileapp4, scanner7), debug APK assembly, and lint with zero errors / six warnings. The new APK SHA256 is F388AF488688DE87811FE32C94D07601DC3DD61ADAC139069ADCFA627E21D01C. Updated emulator UI acceptance is pending.
Follow-up: diagnostics now also collect fixed native QQQ/device request outcomes through an optional, exception-isolated observer (status and latency only). Added regression cases for HTTP errors, transport failure, observer exceptions, and category formatting. Emulator confirmed F388 branding/login/Connected footer/diagnostics; observed Android15 system-bar overlap corrected with safe-drawing insets. Final verification for these follow-ups is pending.
2026-10-29: Implementing QQQ Mobile scanner host and native host extensions. First target is Zebra TC22, online only. Backend companion is `qbit-mobile-scanners`; metadata-defined apps/processes remain the native execution model. Device approval is mandatory. Local demo uses QQQ TABLE_BASED auth on QQQ4.1.0-RC.20.
Implemented server selection, per-server enrollment credentials, approval gate, SCANNER widget DataWedge input, explicit same-event retry, native password login, host extension points, and removal of session/token logging from touched authentication paths. No commit or push.
Validation passed: 19 JVM tests (core12, mobileapp4, scanner3), debug APK assembly, APK signature verification, and lint with zero errors. Five lint warnings remain: three inherited Navigation custom-check compatibility warnings and two dependency/catalog notices. Real sanitized demo HTTP metadata/process/init fixtures deserialize through the native DTOs. Server logout, HttpOnly-cookie login, and per-origin header isolation have regression tests. Physical TC22 acceptance remains required.
Previous pre-branding debug APK SHA256: B3E160CE3279C369CEDB425AC6E777A2729CE8A2E430E7D6F46D35C6376F8214.
Repository readiness added: pinned GitHub scanner tests/lint/APK artifacts and optional emulator workflow; CODEOWNERS, contribution/security/conduct guidance, issue/PR templates, Dependabot, changelog, editor/line-ending policy, and scoped agent instructions. `actionlint`, `shellcheck`, `shfmt`, and `git diff --check` passed. Upstream main `63fca89` was fast-forwarded and all implementation files restored without conflicts; the managed CircleCI Android profile and QRun-IO Maven registry updates are preserved. The full Gradle verification command passed again after reconciliation (16 executed tasks, remaining unchanged tasks up to date). Remote CI and emulator acceptance have not yet been recorded here.
+13
View File
@@ -0,0 +1,13 @@
# Hardware test barcode
Open this page on another screen or print the image at its original proportions. Scan it with the handheld's physical trigger while **Scan capture** is ready.
![QR code containing the literal string 001234567890](test-barcodes/leading-zero-qr.png)
[Open the full-size QR image](test-barcodes/leading-zero-qr.png).
The exact expected value is **`001234567890`**. Preserve it as a raw string, including both leading zeros; do not convert it to a number. This image contains only that test value, with no enrollment URL, device token, or credentials.
Wait for **Saved scan**, then verify the new backend scan record contains the exact value `001234567890`, QR symbology, `zebra-emdk` source, the expected device, and a new event ID. Match its capture time to the trigger pull. Seeing or decoding this image alone does not establish successful hardware capture or storage; record that acceptance separately. A deliberate second trigger pull should produce a distinct event ID.
The 420 × 420 PNG was copied byte-for-byte from the verified local fixture and independently decoded with ZXing 3.5.4 after copying. SHA256: `81ADBEE44E4F3ACA8DE59845151134C433DFD76270A45D42872835F214BC0F8F`.
+125
View File
@@ -0,0 +1,125 @@
# Third-party release review
Review date: 2026-10-31. This is source, cached dependency metadata and APK evidence, followed by authorized vector provenance remediation. It is not license clearance or a final release SBOM. No Gradle build was run for this review. The source was changing during inspection; final release outputs must be reviewed again after dependency and asset remediation.
The inventory below is historical checkpoint evidence. The current candidate replaces Auth0's beta with stable 3.0.0 and Coil's RC with 3.0.4; see [the dependency decision](DEPENDENCY-DECISIONS.md). Auth0's tagged stable LICENSE matches the bundled MIT text byte-for-byte, including its copyright. Post-change packaged graph/notice qualification is pending: an initial build correctly rejected stale generated Auth0 metadata, exposing a missing dependency-graph input in the pinned AboutLibraries collector. The build wiring now tracks resolved coordinates; a subsequent successful build and APK inspection must establish the new inventory rather than reusing these old counts or hashes.
## Evidence boundary
- Repository baseline: `6574ec4991dbb859b60c3a0be462af96918aee32`, plus the working changes.
- Cached debug package graph: `scanner-host/build/intermediates/lint_report_lint_model/debug/generateDebugLintReportModel/debug-artifact-dependencies.xml`, written 2026-10-31 03:44:28 workstation local time. SHA-256: `4C482AE3C0D20DF83D9780A924C58AC23B4B311A56439F6F059FED6CE532F870`.
- That graph has 123 package entries: two project modules and **121 external coordinates**. Compile-only and test graphs are different; this count comes from its `package` section.
- Inspected cached debug APK: `scanner-host/build/outputs/apk/debug/scanner-host-debug.apk`, 17,852,539 bytes, written 03:13:23. SHA-256: `36FFA7C5F405433A2EBE49BDEB3909A1D12E6998B77F1BDA0E1326118E709382`.
- The graph is newer than the APK. They are separate observations, not proof that the exact graph generated that APK. There was no cached signed release APK qualified by this review.
- All 121 coordinates had cached Maven POMs. Their declarations were inspected with XML tooling; this was an inspection, not a replacement license-report generator.
## Runtime inventory and declared licenses
The cached POMs directly declare Apache-2.0 variants for 119 coordinates and MIT for Auth0. `com.google.guava:listenablefuture:1.0` inherits its license from the cached `com.google.guava:guava-parent:26.0-android` POM, which declares Apache-2.0. Thus all 121 top-level artifact declarations were accounted for. This does **not** cover embedded data, native subdependencies, copied resources, missing upstream copyright notices or every transitive source file inside an artifact.
| Component family | Observed packaged versions | Primary local evidence / declared license |
| --- | --- | --- |
| AndroidX UI and Compose | UI/runtime/foundation/animation 1.7.2; Material3 1.3.0; ripple 1.7.0; icons/tooling preview 1.6.6 | Resolved package graph and exact cached POMs; Apache-2.0 |
| AndroidX platform libraries | Lifecycle 2.8.6, Navigation 2.8.3, Activity 1.9.2, Core 1.13.1, AppCompat 1.7.0, DataStore 1.1.1; other AndroidX support modules remain in the graph | Exact cached POMs; Apache-2.0 |
| Material Components | `com.google.android.material:material:1.12.0` | Exact cached POM; Apache-2.0 |
| Coil | Seven resolved Android/JVM artifacts at **3.0.0-rc01** | Exact cached POMs; Apache-2.0 |
| Auth0 | `com.auth0.android:auth0:3.0.0-beta.0` | Exact cached POM declares MIT and points to the upstream license; preserve actual copyright/license text, not just the label |
| Retrofit/converter | `com.squareup.retrofit2:retrofit:2.9.0`; `com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0` | Exact cached POMs; Apache-2.0 |
| OkHttp/Okio | OkHttp and logging-interceptor 4.12.0; okio-jvm 3.9.1 | Exact cached POMs; Apache-2.0; embedded Public Suffix List requires separate treatment below |
| Kotlin runtime | reflect and stdlib **2.0.20**; stdlib-jdk7/jdk8 1.9.0; parcelize and android-extensions runtime 1.9.22 | Exact cached POMs; Apache-2.0 |
| kotlinx | Serialization core/json JVM **1.6.3**; coroutines core JVM/Android 1.9.0 | Exact cached POMs; Apache-2.0 |
| Other resolved artifacts | accompanist-drawablepainter 0.36.0; androidbrowserhelper 2.4.0; AndroidSVG AAR 1.4; Gson 2.8.9; annotations 23.0.0; error_prone_annotations 2.15.0; listenablefuture 1.0 | Exact cached POMs, including inherited Guava declaration; Apache-2.0 |
Do not derive the release inventory solely from `libs.versions.toml`: declared serialization 1.5.1 resolves to 1.6.3; declared Lifecycle values resolve to 2.8.6; Compose resolves a mixture of versions. The current catalog and explicit dependencies explain inputs, not the final selected graph.
### Additional content inside the APK
The inspected APK preserves `okhttp3/internal/publicsuffix/NOTICE`. It identifies `publicsuffixes.gz` as compiled from the Public Suffix List and names **MPL-2.0**, with source and license URLs. Treat that embedded data separately from OkHttp's Apache POM declaration. Keep its notice and verify the relevant source/license delivery for the exact packaged version; do not summarize the complete app as containing only Apache/MIT material.
The APK also contains `libandroidx.graphics.path.so` and `libdatastore_shared_counter.so` for four ABIs. The corresponding AAR declarations are known, but native embedded notices/subdependencies have not been independently inventoried. That is an explicit remaining review item, not a claim of an additional license or incompatibility.
## Notice delivery gap
`NOTICE` now describes QRunIO, its trademarks and the pinned Google Material resources. It is not a complete dependency inventory. `scanner-host/build.gradle.kts` excludes `/META-INF/{AL2.0,LGPL2.1}`. This exclusion is not evidence that the graph includes LGPL code, but removing generic license resources requires a reviewed replacement notice-delivery path.
The inspected APK has no named app-wide license/NOTICE/report payload or OSS-license resources; only the Public Suffix List notice was found by archive entry inspection. No AboutLibraries/OssLicenses integration or user-accessible licenses screen was found in the inspected source. Repository-root LICENSE/NOTICE files alone do not establish that the distributed APK carries the needed third-party notices.
Release work must generate a reviewable report from the **release variant**, package the reviewed notices, expose them from the app's About/licenses entry, and retain the same report alongside release assets. Verify this offline on the final signed bytes. Check exact copyright/NOTICE content as well as SPDX labels.
## Vector resource provenance
Git commit `4cb65dc1d1d1f681c268513dc1c6c8d7b2f0d89c` introduced the drawable set on 2024-10-30 with the message “Initial checkin - of all material icons ... and some default sound effects.” The sampled files already had project boilerplate at introduction; that boilerplate is not evidence of exclusive project ownership.
The review pinned Google's last repository revision before that commit's timestamp: [`google/material-design-icons` at `d7a78a2ad434d5d38e1b8708c9dd9847cc3ada9b`](https://github.com/google/material-design-icons/tree/d7a78a2ad434d5d38e1b8708c9dd9847cc3ada9b). Its complete `src` tree contains 2,170 filled 24px icon definitions. All corresponding Android XML vectors were downloaded read-only into the workstation cache from that fixed revision.
For each local drawable, the ordered `android:pathData` strings were joined with a newline and hashed with SHA-256, then compared against all pinned upstream vectors. This compares drawing instructions only: it does not claim whole-file identity, identical tint/fill, or a mathematical proof of equivalent paths for nonmatching representations.
- Before remediation, **2,094 of 2,142** local drawables exactly matched a pinned upstream path-data hash.
- This includes 31 renamed/alias resources, such as `__ft_apart` → `6_ft_apart`, `_d_rotation` → `3d_rotation`, `ic_broken_image` → `broken_image`, and `resource_public` → `public`.
- Local files commonly remove Google's theme tint and replace the fill with black. Record these as modifications while retaining the appropriate upstream provenance.
- **46 named Google icons** had nonmatching path-data representations. Samples showed absolute/relative command changes and circle/arc transformations; the review did not infer an exact match from visual similarity or a filename.
- `loading_img` and `qqq_no_network` were the other two unmatched resources. They needed provenance or replacement.
The comparison manifest is in `C:/Users/james/.cache/qqq-material-icons-pinned-20241030/all-geometry-comparison.json`, SHA-256 `2B36EBE19CABF189370A1EBF9A1D8946EF04A467AC177108D1B61CA60D150125`. The same directory contains the 2,170 pinned upstream XMLs and download URLs; the cache is review evidence, not shipped attribution. Google's repository supplies an [Apache-2.0 license](https://github.com/google/material-design-icons/blob/d7a78a2ad434d5d38e1b8708c9dd9847cc3ada9b/LICENSE). Preserve the pinned upstream license and modification/provenance record when adopting these assets.
The 46 nonmatching named resources are:
```text
account_circle assignment_add barcode_reader bed bedtime_off bedtime cached
check_circle construction conveyor_belt desktop_mac desktop_windows dew_point
directions_off directions emoji_symbols forklift format_list_bulleted_add
front_loader gavel hotel_class keyboard_hide kitchen live_help local_gas_station
local_offer movie_edit new_releases nights_stay pallet percent person_pin_circle
rebase_edit recycling school screen_search_desktop shelves shopping_bag snowing
sports_cricket star sunny_snowing trolley tungsten wb_twilight window
```
### Completed source remediation
The 46 named vectors were replaced with pinned upstream counterparts, using the application's black-fill/no-theme-tint convention. The existing `loading_img` resource now uses pinned `hourglass_empty` artwork, keeping the Coil placeholder resource ID without changing UI code. `qqq_no_network` now uses pinned `wifi_off`, keeping its resource ID and blue fill. Every resource retains its original dp width and height; a comparison against Git HEAD found zero dimension differences across all 2,142 resources. Viewports for replacement geometry come from the corresponding upstream vector.
All 2,142 resources now have Google original-artwork attribution, the exact pinned source URL and project integration/modification attribution. Their final drawing path-data hashes all match the selected pinned originals. [The checked-in manifest](material-icons-provenance.json) records each resource, upstream path, upstream file SHA-256, final local file SHA-256, ordered path-data SHA-256 and final styling. Every local file hash was verified against that manifest. [The exact upstream license](third-party/material-design-icons-LICENSE.txt) and the updated repository NOTICE are retained.
`loading_img` remains used by `ImageWidgetBlock.kt`; `qqq_no_network` remains used by `NoNetworkDialog.kt`. No UI code or caller names were changed by this resource remediation. This adopts known upstream artwork with its existing license and identifies modifications; it does not relicense Google artwork as exclusively project-owned. The authorized asset update is complete and ready for the serial Android build. Final APK packaging and visual acceptance remain separate gates.
## Recorded audio
The same introduction commit added three recordings. Their metadata has no author/source/license attribution; encoder/container metadata is not provenance.
| File | Use observed before remediation | SHA-256 |
| --- | --- | --- |
| `boop1.mp3` | Submit feedback in `QProcessHome.kt`; approximately 1.485 seconds | `990D784B32369B6A67CAC1A11AF8AEBD5483A0ADB2951D23A34FF5170FFCFED2` |
| `tick1b.mp3` | Loading-step tick in `ProcessStepLoading.kt`; approximately 0.131 seconds | `DEF0D5340E38E247699E70659BDAB66CF371B91E5C0206D10682794F2380CB79` |
| `tick1.mp3` | No source use found; approximately 1.485 seconds | `08672F3E764EC68A28E0130FDDB3E820F43F06E460D38766F3B59691258861FE` |
All three remain in the inspected **older APK**. During this review, the implementation owner removed the source files and changed the two call sites to platform-generated `ToneGenerator` feedback through the existing `ProcessSound.Playback` lifecycle: submit acknowledgment for 80ms and loading beep for 25ms. This preserves feedback intent without distributing unknown recordings. A rebuild must prove the files are absent from the final APK; hardware audibility, system volume behavior and lifecycle disposal still need acceptance. This source change is not an artifact-level clearance.
## Version stability and Kotlin alignment
Coil 3.0.0-rc01 and Auth0 3.0.0-beta.0 are actual packaged prerelease dependencies, not merely unused catalog entries. The shared library still references Auth0 even though the first scanner server uses table-based authentication. Review supported stable replacements and test authentication, SVG/image loading and scanner/native flows before adopting changes; a version label alone is not proof of a vulnerability.
The precise Kotlin mismatch is **compiler/plugin 1.9.0 versus runtime reflect/stdlib 2.0.20**, not reflect 2.0.20 against an older main stdlib. Compose compiler is 1.5.1; runtime helper artifacts also include 1.9.0 and 1.9.22. `qqq/android/core/build.gradle.kts` explicitly declares reflect 2.0.20, and `GenericSerializing.kt` imports reflection APIs. Do not blindly force every artifact down to 1.9.0 or call this a proven crash. Choose a supported compiler/Compose/runtime combination, inspect release `dependencyInsight`, then run the existing serializer and native suites. This review ran no compatibility build and makes no unsupported claim that the current mix is safe or broken.
## Standard reporting approach
The scanner now integrates **AboutLibraries 10.9.2** for standard variant-specific definitions and native Compose display. Source and configuration are in [the supplemental metadata directory](../scanner-host/third-party/README.md). The selected upstream source is pinned to `2ff232b9b40c24b8afa23494bc7077e6258b89fe`; it uses Kotlin 1.9.20/Compose 1.5.4/AGP 8.1.2 upstream. Local qualification against this project's toolchain is a separate recorded gate. This introduces no project-specific POM/license parser or HTML/JSON notice renderer. [Pinned upstream AboutLibraries documentation](https://github.com/mikepenz/AboutLibraries/tree/2ff232b9b40c24b8afa23494bc7077e6258b89fe).
The integration supplies full local Apache/MIT/MPL text and distinct Google Material icon, QQQ project and Public Suffix List notices. The exact Auth0 MIT copyright is retained. The plugin's network license/funding lookups are disabled; the runtime viewer reads its packaged resource. AboutLibraries strict mode only checks recognized license IDs, so the build also validates its generated standard output against the actual Gradle runtime coordinates/versions and rejects absent license references or empty text. Every scanner variant runs this validation before resource merging. The distribution copy is `scanner-host/build/reports/third-party/<variant>/aboutlibraries.json`, with project LICENSE/NOTICE alongside it. The new viewer's isolated instrumentation opens the Google notice from the generated APK data and checks its actual rendered text.
The implementation should:
1. Generate the scanner **release** dependency/license report with the chosen tool, excluding test-only artifacts; preserve selected coordinates and exact output with the source/APK hashes.
2. Review full license/copyright/NOTICE text and inherited POM declarations. Add pinned Material icon provenance, Public Suffix List data and any native embedded notices through the tool's supported supplemental mechanism or a simple reviewed asset notice document.
3. Package and expose the reviewed report using the tool's native UI. Include project LICENSE/NOTICE separately as appropriate; do not replace third-party copyright with project boilerplate.
4. Make missing/unresolved notice metadata a release review failure, then verify the final signed APK contains the intended reports and that the licenses screen opens offline.
5. Regenerate after stable dependency selection, Kotlin alignment and resource replacement. A cached debug inventory cannot certify a later signed release.
No legal clearance, final dependency security clearance, asset relicensing or production release approval is asserted by this document.
## Qualification update — 2026-10-31
The AboutLibraries integration passed the current scanner build, 33 scanner JVM tests, scanner lint (zero errors/warnings), debug/E2E APK assembly and shared native-test compilation. The isolated TC22 `licenses` scenario subsequently passed: it opened generated entries and rendered the full Google notice using packaged resources without a server. Release signing remains deferred by the owner.
Negative generated-report checks independently removed a real runtime entry and blanked Apache license text; each failed with its intended error. The generated report was restored byte for byte after each test, then all three variants passed positive verification. The device-tested E2E APK remained unchanged throughout those checks.
[The native binary review](third-party/NATIVE-NOTICES.md) closes the earlier `.so` inventory gap with exact published AAR/source pins, per-ABI hashes, embedded NDK identity and full startup/runtime/header notices. It adds two distinct NDK entries and merges the native copyright notices into the two existing AndroidX dependency entries. Current debug, release and E2E reports each verify **125 external runtime dependencies and 130 notice entries**. Final bounded packaging passed for debug, E2E/test and unsigned release APKs, with zero release lint errors/warnings. Every final host APK contains the exact validated 130-entry report, all nine complete license texts and the eight unchanged upstream native binaries; the unknown recordings are absent. SDK tools verified release non-debuggability, disabled cleartext/backups and the expected unsigned state. The provenance manifest records the final APK/report hashes separately from historical device-test evidence. This update does not resolve the separately documented prerelease dependency/toolchain alignment or production signing/distribution gates.
+131
View File
@@ -0,0 +1,131 @@
# TODO
Latest November 3 checkpoint (current; supersedes the older entries below):
- [x] Android `f72b90d` the signed application-code commit published in PR 10 (SSH read-back verified); Gitea CI 3480 succeeded (unit-test, device-test, package, secrets, gate); clean merge-tree with `main` b75fdac, nothing merged. Pull requests #8 and #10 remain open; merge and release not done. This update changes documentation only.
- [x] Fresh disposable TC22 Echo ten-stage run: 109/109 semantic requests (simulated inputs/printing); installed APK `4AAB6951...AB850E` unchanged data.
- [x] Fresh real Authentik desktop acceptance passes; native harness compiled, 8 launcher guards pass. [ ] On-device run awaits the temporary CA and PIN; no trust bypass.
- [x] Server-side RC.21 gate PASS: 377 unique tests plus 15 real admin-browser E2E on public RC.21; the earlier MySQL timeouts did not recur. [ ] It is public core RC.21 with qualified private owner inputs, not a public/released cohort; the normal runtime core is not upgraded and Echo on RC.21 is not installed.
- [x] Install final client over the normal TC22 with signature match/data preserved; existing sign-in/approval and Connected verified. Settings include portrait, location reporting and working wallpaper preview.
- [x] Fresh 222 JVM tests, lint zero errors/warnings, all APK builds, 23 Node tests and 255-file standards gate; updated ten-stage native Echo acceptance passes with simulated scans/printing.
- [x] Integrate and compile opt-in real Authentik Android acceptance; real-provider desktop acceptance rerun passes all six phases with strict TLS.
- [ ] Run real native Authentik after temporary CA installation/device interaction. Confirm physical scanner/label count/fidelity, camera/GPS/projection/profile behavior. Complete shared artifact publication/hosted CI; signing remains last.
Current checkpoint (November 2, copies adoption; supersedes older source/runtime notes below):
- [x] Verify optional Copies per scan through the actual generic native UI: default 1, range 1–100, selected 3 retained across two scans/recreation, and Apps/Leave; ten stages, 116 documented requests. Normal TC22 setup shows 1 against the updated local server.
- [x] Combined candidate gate: 222 JVM tests, lint zero errors/warnings, debug/E2E/test APK compilation and 15 Node tests. Normal installed APK remains D8109C61; newer feedback/profile/wallpaper/AIM code is not installed.
- [ ] Physically confirm label quantity and barcode fidelity. Native Authentik and camera/GPS/projection qualification remain open while the operator is remote; temporary CA installation is unconfirmed.
- [ ] Coordinate canonical Generation copies adoption and coherent publication/CI. Signing/distribution remains last.
- [x] Implement negotiated mobile1.5 raw-byte transport with immutable capture, same-server capability gating and byte-identical failed-request retry;185 JVM tests, lint0errors/0warnings, debug/E2E builds and QA test compilation pass.
- [ ] Qualify new raw-byte delivery on TC22 and complete actual AIM extraction/decoder-profile checks. Current source leaves AIM absent and does not change hardware configuration; full Echo fidelity is still open.
- [x] Verify native `Origin` compatibility:174 JVM tests, lint0errors0warnings and APK build pass; normal TC22 login/approval/heartbeat/Echo setup/printer-media choices/Cancel-Leave pass against the strict local Echo host through8080->57832, preserving data.
- [x] Verify generic INTEGER Document choice/dependency clearing on actual TC22:7 stages,112 semantic requests,99 focused/host tests and8 Node guards; simulated printing, normal data preserved.
- [x] Connect the installed print client to the single local server and publish the actual Echo4x1 document through native QQQ processes.
- [x] Resolve the printing agent's earlier 4x6 media report versus loaded 4x1 stock: the Printing owner aligned 101600 x 25400 micron/300 dpi media and the TC22 selected the real printer, fresh 4x1 media and published document; Next succeeded.
- [ ] Complete one physical Code128 (QQQ-TEST-123456) scan, print and rescan on the TC22, then leading-zero QR. One separate synthetic HTTP Echo event reached the actual Windows spooler; physical paper and handheld print/rescan remain unconfirmed.
- [ ] Qualify public coherent artifacts and primary Gitea CI; signing and distribution stay last by user decision. Core PR 1337 normal CI and publication are not established; do not repin default dependencies.
- [x] Add metadata-driven Echo acknowledgment/manual-entry hints with old defaults; 167 JVM tests, strict compilation, lint zero errors/warnings and debug build pass.
- [ ] Qualify native Echo scan/print-acceptance with independently decoded physical output (setup and Leave against the real owner adapters are verified; source tests are not physical printing evidence).
- [x] Verify Query artwork, Dogfooding/Query links and licenses on the normal TC22 About screen using filtered accessibility data.
- [x] Apply scoped copyright/import/header/formatter corrections, mechanical CI gate and Kotlin warnings-as-errors;163JVM plus46TC22 sample native cases pass. See [review](CODING-STANDARDS-REVIEW-2026-11-01.md) for the remaining formatter CI limitation.
- [x] Add Query artwork, app version, Dogfooding/Query links and license access under Settings → About QQQ Mobile.
- [x] Qualify real Authentik desktop nonce rejection and retained-browser A → company logout → fresh B; actual Android callback acceptance remains separate.
- [ ] Resolve the explicitly retained [legacy source follow-ups](LEGACY-SOURCE-FOLLOWUPS.md): metadata typing, renderer contracts, navigation/action validation and build identity. Removing inline work markers does not implement these features.
- [x] Replace the raw diagnostics popup with tested full-screen status/activity, filtering, pause, copy/share/clear and Back; 159 client JVM tests, zero-warning lint/build and the focused TC22 diagnostics UI test pass.
- [x] Default foreground location reporting on, preserving explicit opt-out and one-time Android permission behavior; settings/policy tests pass.
- [x] Verify current trusted EMDK Code128, QR leading zeros and menu/reopen on installed checkpoint1c766bf/E34AC3C6.
- [ ] Observe the diagnostics/location update on the normal package, grant/deny/restore location permission and verify an actual provider fix; keep physical lifecycle/outage/retry and provider acceptance gates open.
- [x] Verify the API1.10 original-nonce and optional company-logout client patch: 151 JVM tests, zero-warning lint/build and 28 disposable TC22 native authentication/lifecycle tests pass; independent 26-file source review completed with no findings.
- [ ] Qualify the paired strict server policy and actual Authentik A-to-company-logout-to-B handoff, including the exported native callback and real SDK/browser logging boundary; synthetic native tests do not close this gate.
- [x] Verify current TC22 Code128 hardware trigger-to-save on the trusted EMDK path with operator Saved scan, matching backend source/model and distinct event identities; see [physical evidence](PHYSICAL-TC22-ACCEPTANCE.md).
- [ ] Finish current EMDK leading-zero QR, repeat-intent, lifecycle/ownership and retry/leave acceptance; historical DataWedge scans do not qualify these checks.
- [x] Verify the synchronized rotation/recreation assertions in both5ba GitHub native runs; one entire run passed and the other reached a later fresh-login timeout.
- [x] Verify final fresh-login readiness at229fce3 and the later portrait-default7de checkpoint: all eleven hosted checks pass, including native navigation/input, PixelCopy and semantic scenarios with a new session. QR documentation2e0fd962 does not qualify physical QR acceptance.
- [x] Make portrait the default for missing/invalid preferences while preserving explicit choices; four focused tests, lint/debug assembly and preserving TC22 install pass, with actual requested portrait orientation confirmed.
- [ ] Finish the operator walkthrough on the newly updated normal TC22 app; private data/enrollment and the development signing identity were preserved.
- [x] Compile the opt-in screen-consent QA harness and ordinary E2E tests; verify early opt-in/protected-port safety gates. Actual permission/display acceptance remains open.
- [x] Compile/review additive consent-based device-screen client and paired1.2 server:137 JVM, package/lint/APK inspection, ordinary-UID TC22 scan-input rejection,39 server/six demo/all15 browser cases and strict provenance pass. Actual projection acceptance remains separate.
- [ ] Execute opt-in actual Android consent, notification, Home/background pixels, Stop, lease expiry, lock/rotation and forced-session-end acceptance on the refined device-screen candidate.
- [x] Supersede the historical hosted Back/focus failure gate with all eleven passing checks at7de; retain the failed-job diagnostics and actual Back/focus regression requirements in history.
- [x] Establish the current EMDK Code128 positive control after the historical failed attempt: eight priorF3AF events and oneC891 event are verified. Preserve the failed-attempt evidence and bounded operator stage; QR/lifecycle/retry acceptance remains open above.
- [x] Resolve normal backend ownership and perform the reviewed API 1.9 upgrade with fresh backup, original-column/data checks, recovery logins and live account validation; preserve the operator APK. Private verification confirms two devices and three scans retained.
- [x] Verify five client audit fixes: password/legacy callback origin binding, independent local Auth0 sign-out, sample release HTTPS and no-backup session storage. The batch passes 109 JVM tests and all 32 selected TC22 native cases; packaged release controls are inspected. See [review scope and evidence](SECURITY-REVIEW-2026-10-31.md).
- [x] Verify the trusted scanner-ingress replacement with production-path ordinary-UID rejection and actual installed EMDK Code128 positive controls. Preserve the historical security report scope; this does not close the remaining physical lifecycle/QR matrix.
- [x] Correct the demo's mixed RC.20 runtime and test-engine dependencies; qualify exact-source core `ecf0f550a0357cec6387c2b54efca128a405a629` with server `b14d925b741e815d1cfa4f62301f19207f25b03f`. Fresh37 server/five demo, strict provenance and all15 nullable-admin/session-monitor/browser cases pass. Core full closure:4,507 discovered/16 existing skips/zero failures/errors.
- [x] Reverify hosted Back/focus behavior at7de with real Back injection and the focus guard; historical failures remain documented in session state.
- [x] Qualify stable Auth03.0.0/Coil3.0.4, notice-generator incremental inputs and overlay/window-focus scan gating:127 JVM,34 shared TC22 cases, an additional eight-overlay native regression, ordinary-UID injection rejection and all package/lint/notice gates pass. See [candidate acceptance](TRUSTED-SCANNER-ACCEPTANCE.md).
- [x] Resolve viewing scope through optional operator-consented DEVICE_SCREEN on mobile1.2 source, retaining default APP_CONTENT and old-server compatibility. Actual projection acceptance remains open; normal mobile1.1 runtime exposes app-content only.
- [x] Verify the unconfirmed-scan exit correction: 46 mobile JVM tests and all 21 selected TC22 native cases pass, including failed cancellation and explicit Leave anyway with preserved receipt/retry identity and in-flight/photo guards.
- [ ] Finish the operator walkthrough of the unconfirmed-scan exit correction on the normal scanner installation.
- [x] Qualify semantic OAuth2 client protocol/policy and generated notices; all seven synthetic browser-result cases and the selected native navigation suite passed on TC22. See [acceptance evidence](NATIVE-AUTH-ACCEPTANCE.md).
- [ ] Complete actual configured Authentik acceptance: real browser/exported-callback round trip, authorization rotation/process death, and next-operator switching with prior browser SSO. See [authentication deployment](AUTHENTICATION.md).
- [x] Verify the single hamburger drawer correction: no duplicate Apps/Switch app controls, fixed Settings/Diagnostics, signed-out gating, drawer Back preserving drafts, and guarded Home/app switching with exactly one cancellation.
- [x] Verify persisted device Orientation choices on TC22, including process draft and pending-photo preservation across rotation.
- [x] Implement and verify [navigation UX](NAVIGATION-UX.md): primary Apps/app/process navigation, secondary Account/Settings/diagnostics, shared Back/leave policy, protected in-flight operations and sign-out lifecycle.
- [x] Prepare independent scanner release workflow and environment-only signing/version configuration; actionlint passes.
- [x] Independently review the exact release workflow, reusable verification and Gradle signing configuration; see [review scope and hashes](RELEASE-AUTOMATION-REVIEW.md). No actionable source finding; external protection settings are not verified by this review.
- [ ] Configure reviewed GitHub release protections and the organization signing identity, verify the actual signed APK and upgrade/migration, and publish only after all [release prerequisites](RELEASING.md) pass. Signing/distribution remains owner-deferred until the final step.
- [x] Correct false legacy network blocking while the scanner's server is reachable; preserve multi-network connectivity for shared clients.
- [x] Persist workstation host-GPU configuration and document cold-start commands.
- [x] Add runtime server branding and a foreground connection footer with bounded sanitized diagnostics.
- [x] Verify customer header/theme, diagnostics and connection loss/recovery on the emulator.
- [ ] Verify customer-specific logo assets and physical TC22 appearance.
- [x] Complete Gradle unit tests, debug APK, signature, and lint verification (26 tests; zero lint errors).
- [ ] Complete the current EMDK physical matrix: QR, lifecycle/ownership, retry/revocation, bad credentials, sign-out and re-login. The completed DataWedge checks below are historical and do not qualify current EMDK behavior.
- [x] Verify TC22 physical Code128 and QR triggers through DataWedge intent output, visible Saved scan, preserved leading-zero QR data and independently inspected backend provenance; two Code128 triggers plus one QR trigger produced exactly three records.
- [ ] Verify release APK signing/distribution separately; current deliverable is a debug APK.
- [x] Reconcile exact historical project-owned source license boilerplate and the IDE template with the existing Apache-2.0 migration; preserve copyright ownership and other notices.
- [ ] Verify third-party asset/dependency attribution and signed release/upgrade delivery before production distribution.
- [ ] Configure and test production identity-provider integration; TABLE_BASED remains supported, and native OAuth2 requires explicit server advertisement and callback registration.
- [x] Implement public `/qqq/v1/version` compatibility checks before credentials or native screens, including required features and the registered mobile-scanners extension.
- [x] Implement semantic process completion, field/step updates, editable/view forms, backend cancellation and explicit unsupported-component blocking.
- [x] Verify semantic migration JVM suites: 53 tests passed (core 21/mobileapp 23/scanner 9), including hidden fields, explicit null clearing, generic event ID isolation and completion when the server omits `nextStep`.
- [x] Verify companion browser E2E after the semantic migration: six scenarios passed in 2.9 minutes.
- [x] Require Android client-channel discovery; companion real HTTP tests use the Android header on every request and pass.
- [x] Complete final JVM/lint/build verification of this semantic API migration: 53 tests, zero lint errors/six warnings, normal debug and isolated E2E/test APKs.
- [x] Verify all six native scenarios across focused runs and the OpenAPI request audit: incompatible core, missing extension, missing Android channel, baseline/additive scans and native administrative forms.
- [x] Identify the TC22 through ADB, authorize USB debugging, install the latest normal APK with login/enrollment preserved, approve its device record and open native Scan Capture with Connected status. Initial Code128 decoding passed; the remaining physical matrix is still pending.
- [ ] Extend the [documented native component boundary](UI-API.md) with remote possible values, record lists, specialized forms and table/report/download screens as required; add conformance and rendered E2E coverage for each new capability.
Offline queue/background synchronization is deliberately outside version one.
- [x] Implement semantic mobile session activity, server termination, demand-based own-app screen viewing and optional foreground location.
- [x] Add capture lifetime/lease/race tests, complete scanner receipt checks, strict maxLength and NavController regression coverage.
- [x] Verify short-viewport login and focused long-form submission after IME Done and dismissal handling.
- [x] Complete integrated session/native administrator termination E2E and all 12 shared TC22 navigation/input/account tests; local emulator remains closed.
- [x] Freeze and review session/whole-device source within the documented security scopes and independent architecture A1–A11 closure. Final release revision/scope and external-runtime gates remain in the release ledger.
- [x] Integrate camera/gallery with Record Files typed routes and guarded optional bridge fixture; synthetic native E2E verifies persisted receipt, rotation and forced-logout cleanup.
- [ ] Complete actual camera-lens/operator acceptance after final runtime installation; synthetic activity results do not prove the OEM camera application flow.
- [x] Supersede the October31 hosted checkpoint gate with all eleven passing hosted checks at7de; retain earlier revision-specific results as history.
- [x] Verify the published push/PR native E2E lanes through the current7de hosted checkpoint; physical operator acceptance remains separate.
- [x] Run the native sample instrumentation suite (six passed on API35).
- [x] Add and run real-server scanner-host E2E: login, pending enrollment, approval, native app/process selection, manual scan persistence and revoked-device denial. Use an isolated app ID and database.
- [x] Run and verify native real-server E2E on pushes and pull requests; current7de hosted checks pass.
- [ ] Complete the final operator walkthrough on TC22 after updating the preserved demo/runtime, including outage/reconnect and real camera capture.
- [x] Implement named semantic process choices and qualify Echo native setup/two scans/rotation/Apps Leave onTC22 (169JVM,8guards,86documentedrequests; printing simulated).
- [x] Real Echo host and owner adapters are composed and running in the single local server (see the Origin item above).
- [ ] Print a physical label and rescan the exact value and type (tracked above).
- [x] Clear scanner feedback on process reset; 57 mobileapp JVM tests, zero-warning scanner lint, debug build and 255-file standards pass. Hosted checks and PR10 → PR8 → main integration remain pending.
# Scoped administrator viewing update — November 2
- [x] Remove app-authored viewing stop controls, verify the existing native session-monitor flow and install on TC22 with app data preserved. Retain the visible indicator, Android consent/system stop controls, server leases and authorization.
+36
View File
@@ -0,0 +1,36 @@
# Trusted scanner candidate acceptance
Current status: this page retains its dated/source-specific checkpoint. Current installed C891/earlierF3AF EMDK Code128 proof is in [physical acceptance](PHYSICAL-TC22-ACCEPTANCE.md); QR and actual projection remain pending. Android7de passes all eleven hosted checks. Older statements about then-pending trigger/CI/scope or unchanged normal APK are historical, not the current release gate. Normal server remains mobile1.1; optional DEVICE_SCREEN requires qualified1.2 source and actual device consent acceptance.
Verified October 31, 2026. This is a source-review and development-package checkpoint, not a production release or actual physical-trigger acceptance.
## Executed checks
- **127 JVM tests** pass: core30, mobile46, scanner51. Debug/release lint and debug, unsigned release, E2E, QA and instrumentation package gates pass.
- **34 native TC22 cases** pass in115.717 seconds, covering navigation, input, authentication lifecycle, host storage, and real PNG/SVG/error rendering.
- **One additional TC22 overlay case** passes in17.948 seconds through the actual Main and scanner widget. Drawer Back, Settings, Diagnostics, Account, Leave, Sign out, a separate window and a parent host gate reject hidden input; dismissal preserves the current draft and permits exactly one new event. The hardware driver in this case is synthetic. One real Android Back closes the drawer without opening Leave, changing the process or cancelling it.
- **A separate ordinary-UID injection case** passes against the production-path QA APK. A distinct-signer application with no permissions sends eight forged broadcasts. A temporary observer proves the actual scan action/payload route; after that observer is removed, no forged scan creates a process step or record. One explicit manual positive control receives a real server receipt. The EMDK driver reaches ready; no physical trigger is inferred.
- Every native request in that server acceptance matches the documented semantic API. The isolated fixture uses core `ecf0f550a0357cec6387c2b54efca128a405a629`, server `b14d925b741e815d1cfa4f62301f19207f25b03f`, UI API1.9 and mobile-scanners1.1. Its separate server37/demo5/browser15 and runtime-provenance gates pass.
Independent review of the final SDK/navigation source found no concrete defects requiring changes. The earlier deep audit remains a scoped historical report for `d1de0bf`; it does not certify every later change or external implementation.
## Inspected artifacts
| Artifact | SHA-256 |
| --- | --- |
| Debug APK | `89e74b7af682f199996322fd271115db1ffaa99e12c2b153a53815025b616e9a` |
| Unsigned release APK | `0e412ea62a8cbbb5f867b70eeff6a1ffb3b5bdab5199576e387684cb517f365b` |
| E2E APK | `80ef10d1109a355fba82f99b48a92d6b1b04907247adc093e0664b5e59f5b694` |
| Production-path QA APK | `cfff47b5a03d578a21fa69583111b23c225a4998f0cf795502d379dcb93ab017` |
All four actual APKs contain131 notice entries, nine complete license texts and the eight reviewed native libraries with unchanged hashes. Auth0 resolves to3.0.0 and all seven Coil modules to3.0.4; no Google identity or Credential Manager dependencies were introduced. Each packaged notice resource equals its generated report byte-for-byte.
Standard SDK DEX inspection finds no defined EMDK SDK implementation classes. Debug, release and QA exclude the synthetic scan action, source label and receiver definitions; E2E contains the expected positive control. All variants disable backup with ten cloud/transfer exclusions. Release also disables debugging and cleartext traffic. The vendor library remains optional and device-provided.
The notice collector now declares its resolved dependency coordinates as an incremental input. Changed versions regenerate standard notices; a subsequent unchanged QA build reuses collection/preparation while the strict runtime/version/license verifier still executes. No license allowlist or runtime-absence check was weakened.
## Remaining acceptance
Actual Code128/QR trigger-to-save, leading zeros, distinct repeated trigger events, recreation/background/resume and scanner-ownership restoration remain physical checks. Ordinary-UID rejection alone could not establish that the hardware feature works.
Configured Authentik, actual camera/GPS/customer assets, coherent optional Record Files deployment, final hosted-client checks and preserving operator upgrade remain separate gates. Current viewing captures QQQ Mobile; entire-handheld viewing scope is awaiting owner clarification. Production signing/distribution is deferred to the final step. Normal operator data and application were preserved during these disposable tests; the emulator remained off.
+90
View File
@@ -0,0 +1,90 @@
# Android semantic UI API contract
QQQ Mobile uses QQQ's versioned semantic UI API to discover apps, render supported native process components and execute their actions. Device enrollment, branding and connectivity use the registered `mobile-scanners` extension of that API. The app has no process-specific scan endpoint or hard-coded Scan Capture process name. Android provides the native controls and scanner hardware adapter.
This defines the client's protocol and rendering boundary. It does not mean that every feature of the QQQ web client is implemented on Android. Unsupported required screen components produce an explicit message and block process submission, rather than silently dropping required input.
## Discovery and compatibility
The host first requests `GET /qqq/v1/version` through a public client with no session cookie, authorization or device credentials. No branding, authentication or protected metadata request proceeds until the response passes these checks:
| Contract | Required advertisement |
| --- | --- |
| Core semantic API | `apiVersion >=1.8.0 <2.0.0` |
| Core features | `version`, `metaData`, `processes`, `processCancel`, `extensions`, `widgets` |
| Client channel | `clientChannels` includes `android` |
| Mobile extension | `name: mobile-scanners`, `version >=1.1.0 <2.0.0` |
| Extension prefix | `pathPrefix: /qbit/mobile-scanners`, resolved under `/qqq/v1` |
Compatibility is cached for the selected server's host lifetime and isolated between server origins. Missing features/extensions or Android channel, malformed discovery or unsupported versions stop startup with an explanation and retry option. Unknown optional properties and feature names are tolerated; supported enum models retain an unknown value so an added enum does not crash JSON decoding. Unknown authentication providers and required UI controls remain explicitly unsupported.
The app sends `X-QQQ-Client: android/qqq-mobile`. The QQQ software release and semantic API version are distinct: clients use discovery rather than inferring compatibility from artifact versions. Current installed-runtime and source-qualification checkpoints are recorded in [session state](SESSION-STATE.md).
## API operations
Core authentication metadata, session exchange, application/process metadata, process initialization, step submission, asynchronous status polling and cancellation all use `/qqq/v1`. A `COMPLETE` response without `nextStep` means the process has finished; a named `nextStep` selects the next native screen. The client also honors an explicit legacy `values.noMoreSteps: true`, applies frontend-step replacements and field updates, and does not infer completion from the last declared screen. A false `noMoreSteps` value alone does not terminate a process. Closing a completed process does not send a cancellation request.
The extension operations are:
| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/qqq/v1/qbit/mobile-scanners/branding` | Public customer appearance |
| GET | `/qqq/v1/qbit/mobile-scanners/status` | Public server reachability |
| POST | `/qqq/v1/qbit/mobile-scanners/devices/enroll` | Authenticated device enrollment |
| GET | `/qqq/v1/qbit/mobile-scanners/devices/current` | Authenticated device approval status |
| POST | `/qqq/v1/qbit/mobile-scanners/sessions/heartbeat` | Session activity, capture availability and optional foreground location |
| POST | `/qqq/v1/qbit/mobile-scanners/sessions/current/end` | End this authenticated mobile session |
| POST | `/qqq/v1/qbit/mobile-scanners/sessions/current/screen` | Upload a bounded JPEG while an administrator's viewing lease is active |
The extension publishes its operation IDs, request/response schemas, errors and scanner vocabulary in the server's merged `/qqq/v1/openapi.json`. See the [server contract](https://github.com/QRun-IO/qbit-mobile-scanners/blob/fix/authentication-metadata-device-headers/docs/UI-API.md) for the authoritative extension details. The former `/mobile/v1` routes have been removed; there is no compatibility fallback.
Core structured errors retain their code, message, field errors and trace ID. Field errors are shown against matching form inputs. Diagnostics retain bounded event codes, HTTP status and latency; they do not record raw error bodies, credentials or scan values. Metadata-provided media URLs are resolved as HTTP(S) resources. Public branding assets use a separate client without QQQ credentials.
## Native rendering boundary
| Area | Current behavior |
| --- | --- |
| App navigation | Server-defined apps and process links, including root processes and section app links |
| Process lifecycle | Init, step submission, asynchronous polling, server-directed completion and backend cancellation |
| `EDIT_FORM` | Metadata field subsets, labels, hidden/editable/required flags, text/password, Boolean, integer/long/decimal, inline possible values, named possible-value sources searched through the core operation `searchProcessPossibleValues` (`POST /qqq/v1/processes/{process}/possibleValues/{field}`, JSON `values`/`processValues`/`useCase: form`; contingent fields honor `possibleValueSourceDependsOn` and `possibleValueSourceDependencyBehavior`, re-query on parent change and drop stale selections) and validation errors; explicit null clearing is preserved |
| Date/time fields | ISO text inputs with format hints and validation; no native date/time picker |
| `VIEW_FORM` | Read-only values for the declared field subset |
| Dynamic metadata | Updated frontend steps and updated fields applied to the active process |
| `HELP_TEXT`, `HTML` | Plain semantic text, with text fallback for markup; no embedded web application |
| `PROCESS_SUMMARY_RESULTS` | Status, counts and messages; link destinations displayed as text, without assuming a web route is a native action |
| Composite/ad-hoc widgets | Existing text, button, input, image, audio, divider and nested composite controls |
| `SCANNER` extension block | Trusted Zebra EMDK/manual input; server title, instructions, labels and maximum length; standard process submission and acknowledgment |
| `RECORD_FILES_CAPTURE` extension block | Server-selected parent record; optional Record Files 1.x capability discovery; native camera/photo picker; checksummed resumable online upload and exact committed receipt |
| Unsupported required controls | Explicit unsupported message; Next and process widget submission blocked |
Record/table browsing and CRUD, reports, app-home widgets, record-list process components, bulk edit/load mapping/review, downloads, remote possible-value lookup, blob input and other specialized components are not implemented by this host. Rich web layout, advanced field presentation and section collapse/role presentation also do not have full native parity. Host authentication supports TABLE_BASED and explicitly advertised native OAuth2 session login through UI API1.10 with required nonce validation; see [the contract and pending verification](AUTHENTICATION.md). Keep handheld processes within the supported component vocabulary until their native implementation and end-to-end tests are added.
The scanner event preserves the barcode string, event UUID and capture timestamp. A retry sends the same event. Successful acknowledgment requires a matching `acknowledgedEventId`, `scanSaved: true`, and a nonblank string `scanId`. Missing or contradictory receipt fields leave the same event available to retry. After the request settles, the operator may explicitly leave with a warning that the scan may already be saved. Confirmed server cancellation clears local retry state; failed cancellation preserves it and offers a separate Leave anyway action to abandon local retry without claiming cancellation or undoing saved records. In-flight requests and pending photos still block leaving. An explicitly invalid SCANNER `maxLength` disables the control; only an absent value uses the default. Scanner retry context is explicit; a generic process field named `eventId` does not activate scanner acknowledgment behavior. Device approval and user permissions remain server decisions, including scans submitted through an already-open process.
## Session activity and screen viewing
The approved, authenticated host sends foreground activity through the registered extension. Heartbeats contain app/process/step names and availability, never form values, barcodes, passwords or credentials. The server derives identity from the verified QQQ session and device. Ending a session in QQQ causes the host to clear its local login when the next protected operation or heartbeat returns an authentication failure. Signing out or changing servers stops capture immediately and attempts to end the current server session before standard logout.
An administrator viewing the ordinary QQQ session record renews a short screen-viewing lease through its standard widget. The host uses PixelCopy on its own app-content rectangle only, at most once per second, bounded to 640×1280 pixels and 256 KiB JPEG. No OS screen, other application or remote control is exposed. Capture requires the resumed, focused, approved authenticated screen, completed rendering and a live lease. Password screens, loading transitions, backgrounding and logout block capture and clear the server's displayed frame. Screen/session state is checked after capture and immediately before upload. Lease timing uses server-relative duration and Android's monotonic clock, not the handheld wall clock. Frames are not saved locally.
The operator sees an informational screen-viewing indicator. The app provides no operator pause or stop controls; administrator viewing remains subject to server leases and authorization. Optional whole-device sharing still requires Android consent and honors system revocation. Foreground location reporting defaults on when no saved choice exists; an explicit saved off choice is preserved. Settings exposes the reporting toggle, precise/approximate Android permission, provider availability and fix status. The host requests coarse and fine Android permission together once, on the first approved signed-in foreground session with reporting enabled. Denial never prevents scans or triggers repeated automatic prompts; Settings links to Android permission settings for recovery. Precise permission permits GPS updates, while approximate permission uses available network location. Device location providers must also be enabled; the reporting preference cannot turn on Android GPS. No background location permission is requested or background tracking performed. Updates stop outside approved signed-in foreground sessions or when reporting is disabled. Heartbeats report coordinates, accuracy and capture time only when a foreground fix exists; QQQ shows the age of the last known observation separately. Earlier clients stored no reporting preference, so a previously missing Android grant is not treated as an explicit reporting opt-out.
## Record photos
The optional `record-files` extension must advertise a supported 1.x version at `/qbit/record-files`. The companion `mobile-record-files` 1.x extension at `/qbit/mobile-record-files` defines the native vocabulary through its public `/contract` descriptor; this is checked without credentials before record capability requests. A `RECORD_FILES_CAPTURE` block supplies `values.parent` with opaque string `tableName` and `recordId`, plus optional title/instructions. The client checks the record's capabilities before exposing photo actions; it never infers a target from process names or form values. This block is self-contained and does not silently advance a process step.
Camera capture uses Android's external camera activity and a temporary URI grant restricted to the app's private photo cache. Successful camera staging automatically starts upload; gallery selection uses the system Photo Picker and an explicit Upload action. No broad media/storage permission or background upload queue is introduced. Original bytes are copied/hashed with fixed buffers, bounded by server limits and the 20 MiB client ceiling, and sent as checksummed binary chunks. Create/finalize retries preserve the original request and item UUIDs. A lost finalize response is recovered through status before displaying Saved; all item IDs, sizes, checksums and retained association identifiers must match the complete receipt. Saved does not imply released content or current download permission.
The process cannot advance or navigate away while a photo is pending. Backgrounding stops transport and retains the in-memory intent for explicit foreground recovery; rotation preserves its ViewModel. Discard recovers an uncertain create before requesting server staging cancellation and never deletes committed originals. Process destruction or logout cancels active I/O and cleans only that model's private temporary files after its writer closes. It does not claim remote staging was deleted when authentication has ended. Recovery across operating-system process death is not an offline queue guarantee; server staging expiry/administrative reconciliation still applies. Native device acceptance of the integrated photo flow remains pending; see [SESSION-STATE.md](SESSION-STATE.md).
## Verification and upgrades
JVM checks cover compatibility rejection before credentials, origin isolation, additive metadata, structured errors, cancellation transport, native form rules and process-state decisions. The companion server checks generated OpenAPI against its committed contract baseline and validates real HTTP payloads.
The native E2E runner in [EMULATOR.md](EMULATOR.md) forwards the real host's requests through a test-only OpenAPI audit. Scenarios cover incompatible core major, missing extension, missing Android channel, the real supported server scan flow, additive future metadata, native administrative forms, session activity/viewing/forced logout, and actual PixelCopy image contents. The audit records operation IDs and safe request metadata, never headers or bodies. Passing runs must prove that all observed requests match published operations and rejected servers receive no credentials. Precise revision boundaries and results are tracked in [SESSION-STATE.md](SESSION-STATE.md); implementation of a check is not evidence that its latest run passed.
Server changes using existing supported components can reach the shared APK through metadata. New required native controls or an incompatible major contract require a shared client update and conformance tests. Customer name, theme and logo changes remain server configuration and do not require customer-specific builds. During this initial migration, update the companion server and APK together; preserve the demo database and install with `adb install -r` to retain local device enrollment.
## Echo scanner presentation hints
Mobile-scanners 1.4 adds optional SCANNER acknowledgmentLabel and manualEntryEnabled. Existing servers retain Saved scan and manual entry. Echo supplies Print queued and false, hiding manual controls because typed text has no decoder-provided symbology. Explicitly malformed labels or nonboolean flags disable the control. These presentation hints never change the matching event/scanSaved/scanId acknowledgment rules, and queue acceptance does not mean physical output completed. No application or process name selects this behavior.
+17
View File
@@ -0,0 +1,17 @@
# Gradle wrapper provenance
Verified October 31, 2026 against Gradle's [published checksums](https://gradle.org/release-checksums/).
| Artifact | SHA-256 |
| --- | --- |
| Gradle 8.7 wrapper JAR | `cb0da6751c2b753a16ac168bb354870ebb1e162e9083f116729cec9c781156b8` |
| Gradle 8.7 binary distribution | `544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d` |
The bundled wrapper now matches the [official 8.7 source artifact](https://github.com/gradle/gradle/blob/v8.7.0/gradle/wrapper/gradle-wrapper.jar).
The distribution URL and its enforced checksum are unchanged. The previous JAR
hash, `e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637`,
matches Gradle's published 6.9/7.0 wrapper family; it was an older bootstrap rather
than evidence of a modified JAR.
This establishes the bundled JAR's upstream identity. It is not an independent
audit of every Gradle class or current dependency vulnerability clearance.
+45
View File
@@ -0,0 +1,45 @@
# Zebra scanner input
Normal debug/release builds obtain scans through Zebra EMDK's device-provided service and private Binder callbacks. A package-targeted DataWedge broadcast is still forgeable by another installed application; destination signature checks do not establish sender identity. Production builds therefore contain no scan broadcast receiver.
The app first requires the system's `com.symbol.emdk` shared library and enabled, preinstalled `com.symbol.emdk.emdkservice/.EMDKService`. Vendor classes are loaded only after these checks, through an isolated wrapper. The optional shared-library manifest allows the same APK to install on non-Zebra devices, where explicit manual barcode entry remains available. The EMDK permission is a normal vendor permission, not a claim of signature-based authentication.
## Lifecycle and process contract
When verified discovery for the current server advertises mobile-scanners 1.5 or newer within major 1, each capture can also send `rawDataBase64` and `aimIdentifier`. The EMDK callback encodes its exact `getRawData()` output once before returning; the immutable event survives HTTP retries unchanged. Older servers receive the legacy event fields. Raw input is bounded to 8192 bytes; null, empty or oversized raw input is omitted rather than truncated. These are SDK bytes, which may include framing, not a claim of original symbol codewords. Current hardware capture leaves AIM absent: no prefix inference or scanner configuration change is implemented. Decoder qualification and native raw-byte acceptance remain required for full Echo fidelity.
The adapter acquires the default scanner only while the server-defined SCANNER widget is visible and its Activity is resumed. HARD trigger reads are armed after IDLE, with one outstanding read. A decoded string is passed unchanged to the existing ScanEvent and semantic process submission, with its EMDK symbology and adapter-owned `zebra-emdk` source. New reads wait until the prior event is confirmed or explicitly abandoned by the existing process flow. Manual events remain `manual`. Neither source label is server-side attestation.
An IDLE callback with no pending read also rearms after a trigger release or timeout without a decode. If IDLE precedes the data callback, accepting that data blocks further input and cancels any read armed in between. A rapid pause/resume during an earlier asynchronous manager-open request can temporarily encounter the one-manager guard; **Reconnect scanner** retries explicitly after the prior manager has released. An unconfirmed release remains blocked until the app restarts.
Null, empty or unsuccessful data collections do not submit a scan or tear down the hardware; subsequent IDLE and pending-read state control recovery. A cancellation collection after a successful decode cannot clear its acknowledgment gate. A successful collection with multiple barcodes rejects the whole batch, shows a one-barcode warning and permits a later single scan. SDK ERROR status, service loss and exceptions still release the hardware and expose recovery. This follows Zebra's [data/status callback example](https://techdocs.zebra.com/emdk-for-android/latest/tutorial/tutbasicscanningapi/); no payload or vendor error text is logged.
Pausing, leaving, logging out or changing server disposes the widget and releases the scanner and EMDK manager. Released generations ignore late status/data callbacks, and a late asynchronous manager-open callback releases that manager immediately. Cleanup attempts every owned resource even if one operation fails; a failed release never reports scanner readiness. Errors show manual fallback without printing scan contents or SDK exception messages.
Hardware input also requires the process surface to be interactive and its window focused. Drawer opening/partial swipes, Settings, Account, Diagnostics and leave/sign-out confirmation suspend input while preserving the active process and draft. Shared navigation and the host supply a nested live availability predicate; the widget observes it to cancel/rearm and checks it again with actual window focus before accepting a callback. This prevents callbacks arriving before recomposition from submitting behind an overlay. Closing the overlay rearms when the process has no pending request. Toolbar and Android Back share the host's live drawer priority, so Back closes the drawer without opening a process-leave confirmation behind it, even when authenticated content registers its handler later. Process Leave and explicit abandonment policy remain unchanged.
Zebra documents that acquiring BarcodeManager disables DataWedge scanning and releasing it restores DataWedge. The app does not edit persistent profiles or disable DataWedge globally. Physical acceptance must verify that another DataWedge application can scan after QQQ leaves/backgrounds. See [Zebra scanner lifecycle](https://techdocs.zebra.com/emdk-for-android/latest/guide/barcode_scanning_guide/) and [runtime ownership/compatibility](https://techdocs.zebra.com/emdk-for-android/latest/guide/about/).
## SDK provenance and terms
The owner explicitly accepted Zebra's EMDK terms on October31,2026. The SDK is proprietary vendor software under the [Zebra EULA](https://techdocs.zebra.com/emdk-for-android/eula/), not an Apache/MIT dependency. This acceptance does not relicense vendor code or constitute a general license-clearance opinion.
The build uses `compileOnly("com.symbol:emdk:11.0.134")` from [Zebra's documented Maven repository](https://techdocs.zebra.com/emdk-for-android/13-0/guide/setup/). Repository access is exclusive to that exact group/artifact. `verifyEmdkSdk` checks the selected SDK in each variant's actual compile classpath before compiler/preBuild tasks, requiring version11.0.134 and SHA256 `e0c6b481c25bf641333bf644de940f1ac7f3fc4f8d5c92ba4ae62ea2252c4e36`. There is no dynamic version or checksum bypass.
The SDK implementation is not bundled. Zebra documents11/13 runtime compatibility; the inspected TC22 has runtime13.0.23.4723. Its system framework explicitly binds the named preinstalled service and registers scan callbacks through Binder. This observation is not an audit of all Zebra firmware. Final APK inspection must confirm vendor implementation classes are absent and the standard packaged runtime notices remain complete. The SDK is a build prerequisite, not a falsely labeled OSS runtime entry.
AboutLibraries10.9.2 also collects compile classpaths, so its standard exclusion pattern matches only `com.symbol:emdk`. The checksum task first rejects that artifact in every resolvable runtime classpath, including tests; an accidental runtime dependency therefore fails before packaging and cannot be hidden by the notice exclusion. No permitted-license list was broadened. The separate SDK documentation records the proprietary terms and owner acceptance.
## Test boundary
Use the [leading-zero QR test image and verification steps](TEST-BARCODES.md) for the physical QR check.
Debug, release and isolated QA builds share `src/production` and the trusted factory. QA uses the `.qa` application suffix, existing debug signing and a separate test manifest for local test HTTP. Build its instrumentation with `-PscannerTestBuildType=qa :scanner-host:assembleQa :scanner-host:assembleQaAndroidTest`. Only `e2e` (the default) and `qa` are accepted instrumentation variants. Neither option changes production signing.
Only `src/e2e` supplies the exported synthetic broadcast adapter; its application ID has the `.e2e` suffix and its source label is `e2e-fixture`. It cannot be enabled in production through an intent or server setting. Existing synthetic payloads keep their fixture action for runner compatibility. The QA variant contains no synthetic ingress, so it exercises the same scanner trust boundary without installing over the operator's app.
`ScannerInputSessionTest` covers duplicate reads, no-decode/empty/canceled completion, multiple-barcode rejection/retry, IDLE-before-data ordering, raw-string preservation, acknowledgment gating, pause/resume generations, late callbacks, reconnect after busy acquisition, vendor pending-read state, read/cancel failures and failed-release readiness. It does not exercise Zebra hardware. Required physical acceptance remains: Code128/QR with leading zeros, repeated triggers, rotation during initial acquisition as well as ready state, background/resume, logout/leave, uncertain acknowledgment retry and DataWedge ownership restoration. A separately signed ordinary application must also send forged scan broadcasts to the foreground production-path app while the backend confirms zero resulting scan records. Shell UID injection alone is not that test.
Implementation/source tests and physical/packaged verification have separate evidence; do not claim the latter from a passing synthetic E2E fixture.
The standalone QA instrumentation class `io.qrun.mobile.scanner.ScannerOverlayTest` exercises the production widget and shared navigation with a private in-process fake driver and synthetic repository. It needs no backend configuration, probe app or operator scan, and is restricted to the `.qa` target. It tests overlay/focus suppression, callbacks before recomposition, preserved manual draft and dismissal recovery; it is not EMDK hardware acceptance. Its empty instrumentation Activity is declared only in the QA manifest and is not exported. The production widget has no intent, server setting or runtime toggle for injecting this driver.
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

+22
View File
@@ -0,0 +1,22 @@
# AppAuth 0.11.1 package verification
Read-only verification on October 31, 2026. This records attribution evidence, not legal clearance. It supplements the earlier 130-entry/native qualification with the new OAuth2 dependency.
- Resolved `net.openid:appauth:0.11.1` AAR SHA-256: `D9FF43B14F7EE8E81BC802148190AD808BEB5389953E6E6FFA53CD5575EA611E`.
- POM SHA-256: `F1D8F1CC0A94E0F6FDF74B8C094F0FF37887B91D5C12B2B7E7B0436F39C76C3D`. It identifies AppAuth for Android, Iain McGinniss, the OpenID repository and Apache Software License 2.0.
- The AAR and nested classes.jar contain no separate LICENSE/NOTICE entry; the AAR contains no native library.
- Upstream tag `0.11.1`, tree `5966cc7efc1c7cacf78598ecae45b5e7d9365f9c`, contains root LICENSE and no separate NOTICE. Its LICENSE blob is `2bb9ad240fa04c8cf706a4901c4807878e90c2dc`, 10,172 bytes, SHA-256 `59899C6091B540582ED617E8EEAAC4919DC985CCFC35459EE9752B699BE5205B`.
- The bundled Apache text contains those complete terms plus the standard application appendix. Different full-file hashes do not indicate missing license terms.
The inspected debug and E2E APKs each contain the generated `res/raw/aboutlibraries.json` byte-for-byte: 131 component entries, comprising 126 external runtime entries and five supplemental notices, with nine complete license/notice texts. AppAuth records its exact coordinate/version, author, description and website/SCM with an Apache-2.0 reference. Every referenced text is nonempty. Settings' existing offline license viewer uses these bundled definitions.
| Artifact | SHA-256 |
| --- | --- |
| Debug APK | `CD048AA081E6F34DC53A17F5C535C10116E7C2CF51E1E0234A3330596E979A25` |
| Debug notice report and APK resource | `F624D7996198AD9A47DBB21D8400E7630DE7A251E4AB808D8860B34C0631F4D8` |
| E2E APK | `608F07B898ACE1F2E91263749C2B46D5F4015828CD8646A3377C14A77506D7C5` |
| E2E notice report and APK resource | `4880E92145CB314BF8D31AAB23E014A38E68B2D9BBF9DF9095BEDFB3E0964A6F` |
Both APKs contain exactly eight native libraries, all matching the prior [native provenance inventory](native-binaries-provenance.json). AppAuth adds none. These hashes identify the inspected bytes; subsequent rebuilds require their own byte verification. The earlier unsigned release APK was not rebuilt for this addition and is not an AppAuth-enabled release. Production signing and distribution remain deferred.
Primary evidence: [tagged upstream source](https://github.com/openid/AppAuth-Android/tree/0.11.1), [tagged license](https://github.com/openid/AppAuth-Android/blob/0.11.1/LICENSE).
+46
View File
@@ -0,0 +1,46 @@
# Native binary attribution review — 2026-10-31
The scanner contains two native libraries for four ABIs each. This review follows the actual binaries beyond their Maven POM declarations. [The provenance manifest](native-binaries-provenance.json) records all eight `.so` hashes, source hashes, exact AAR URLs/hashes, and the inspected APK hash. Every inspected APK native entry matches its official Google Maven AAR byte for byte. No native library was rebuilt, replaced or relicensed.
| Published artifact | Cached and independently downloaded AAR SHA-256 | Release source revision |
| --- | --- | --- |
| `androidx.graphics:graphics-path:1.0.1` | `8CA4032B6D79B351F0B59AD4B580EDDBB9423E1652F7C958830687F1EEE2EC03` | `8a05a22af450d589ef911d772a001a49dcb05b71` |
| `androidx.datastore:datastore-core-android:1.1.1` | `883ABC911E4B14119959E44E20A520523F59C303E834D6B325007B3EE5E9CB6C` | `4e91af5f5597ac6275559f2ce4bc2f5cc852bddb` |
The source revisions come from Google's exact-version release links: [Graphics Path 1.0.1](https://developer.android.com/jetpack/androidx/releases/graphics#graphics-path-1.0.1) and [DataStore 1.1.1](https://developer.android.com/jetpack/androidx/releases/datastore#1.1.1), both released May 1, 2024. Their source JARs omit the C++ implementation, so the review also read the pinned upstream native sources and build files. This establishes a published artifact/source association, not an independently reproduced build or compiler attestation.
## Native implementation and included code
Graphics Path builds `Conic.cpp`, `PathIterator.cpp` and `pathway.cpp` with local headers. Its included `math/vec2.h`, `TVecHelpers.h` and `compiler.h` are Filament math headers under Apache-2.0 with Android Open Source Project copyright notices from 2013 and 2017. The path/conic implementation retains 2022 and 2006 AOSP notices. All examined implementation and local headers are Apache-2.0; the minimum Skia structure declarations in `Path.h` do not establish that a separate Skia binary is distributed. [Pinned native tree](https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/).
DataStore builds `shared_counter.cc` and `jni/androidx_datastore_core_SharedCounter.cc`, with its local header and NDK `<atomic>`/JNI headers. Its implementation is Apache-2.0, copyright 2022 AOSP. The exact NDK JNI header is Apache-2.0, copyright 2006 AOSP. [Pinned native tree](https://android.googlesource.com/platform/frameworks/support/+/4e91af5f5597ac6275559f2ce4bc2f5cc852bddb/datastore/datastore-core/src/androidMain/cpp/).
AboutLibraries supplemental records merge these notices into the existing two dependency entries, placing the native copyright/full Apache text first so its stock viewer displays it. The Maven artifact license remains Apache-2.0.
## Toolchain and startup support
The standard `readelf.py` from pyelftools 0.32 inspected dynamic dependencies, symbols, `.note.android.ident` and `.comment` for every ABI. All eight binaries identify **NDK r25c, build 9519653**, with Android Clang 14.0.7 (`9352603`, based on `r450784d1`) and LLVM source revision `4c603efb0cca074e9238af8b4106c30add4418f6`. Google's [pinned NDK prebuilt](https://android.googlesource.com/toolchain/prebuilts/ndk/r25/+/1622e087e1b881a0c7c744091d314662d12f0997/source.properties) confirms version `25.2.9519653`. Its ARM64 API21 `crtbegin_so.o` has the same Android build-identification note and compiler identity as the inspected native libraries.
All eight ELF `DT_NEEDED` lists contain only `libm.so`, `libdl.so` and `libc.so`. The APK contains no separate `libc++_shared.so`. Graphics Path explicitly uses `-nostdlib++`, disables exceptions/RTTI and removes unused sections in its [pinned build configuration](https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/build.gradle). These facts do **not** prove that no header-generated or statically linked compiler support exists; the following notices cover that support separately.
- **Bionic startup/termination/build-identification objects:** the `crtbegin_so.c`, `crtend_so.S` and `crtbrand.S` source headers retain BSD-2-Clause notices, copyright 2012, 2013 and 2015 AOSP. The included `atexit.h` and `__dso_handle_so.h` share the 2012 notice; `pthread_atfork.h` uses Apache-2.0 (2015 AOSP), and its body is conditional on API23 or a forced CRT flag. The [pinned reference sources](https://android.googlesource.com/platform/bionic/+/0b981d1ec055efaef86d8a5d402cf1b3167ba10b/libc/arch-common/bionic/) and the exact NDK's NOTICE corroborate these terms. [The retained BSD notices](native-AOSP-BSD-2-Clause.txt) appear in the separate **Android NDK Bionic startup support** viewer entry. This does not claim that the device's system libc/libm/libdl implementations are shipped by the application.
- **LLVM C++ headers and compiler runtime:** the exact NDK `<atomic>` header declares `Apache-2.0 WITH LLVM-exception`. The [matching LLVM source](https://android.googlesource.com/toolchain/llvm-project/+/4c603efb0cca074e9238af8b4106c30add4418f6/) provides full libc++ and compiler-rt licenses, including the object-code exception and retained historical terms. Both complete upstream files are retained as [libc++](native-LLVM-libcxx-LICENSE.txt) and [compiler-rt](native-LLVM-compiler-rt-LICENSE.txt), and appear together in the **Android NDK LLVM runtime and C++ headers** entry. Including them avoids relying on an inferred absence of embedded support or applying the compiler exception as a blanket waiver. It does not claim the whole LLVM toolchain is bundled.
The corresponding SPDX allowances are restricted to those exact supplemental entries. The build also pins this native review to Graphics Path 1.0.1, DataStore Core Android 1.1.1 and NDK 25.2.9519653. Changing either dependency requires renewed native/source/toolchain review; a new dependency cannot inherit the BSD or LLVM allowance silently.
## Verification and limits
Before adding the native notices, negative build checks removed one real runtime dependency and then blanked Apache license text in generated output. Each failed for the intended reason; generated JSON was restored byte for byte, all three positive variant checks passed, and the already-tested E2E APK stayed unchanged. After the native additions, debug, release and E2E metadata checks pass with **125 resolved runtime dependencies and 130 notice entries**, including five supplemental components. The two native dependency overrides merge existing entries instead of inflating the runtime count.
Final bounded packaging passed on 2026-10-31: debug APK, E2E APK/test APK, unsigned release APK and `lintRelease` (zero errors/warnings), 273 tasks in 2m32s. The manifest retains the earlier device-tested APK as historical native-byte evidence and separately records all final package hashes. Each final host APK contains **130 entries and nine complete license/notice texts**, byte-for-byte identical to its validated variant report. All eight native hashes match the reviewed AARs in every APK; no MP3 recordings remain. Release resource optimization renames the notice file to `res/M7.json`; SDK `aapt2 dump resources` resolves it from `raw/aboutlibraries`, so verification does not assume the debug ZIP path.
| Final artifact | SHA-256 |
| --- | --- |
| Debug APK | `E12CFB7F078AD0C5F28EED717880187F394379C9532EA2D5828A228D1E4A134F` |
| E2E APK | `733285E6BE30B67A46BF2810F07376379C94E4AE529746F321262AB86CCF0740` |
| E2E instrumentation APK | `E697023F2F31F98F5F4A9A82E4A91FE8A8F5B48B08687DD784FE9960947EED35` |
| Unsigned release APK | `924D7EC15F93957073F0D9B47348F0E4CF7F272FBB4EBE4100FEDCBD8DB027A2` |
SDK `apkanalyzer` confirms the actual release APK is non-debuggable; its decoded application manifest sets `usesCleartextTraffic`, `allowBackup` and `fullBackupContent` to false, with no overriding network security configuration. SDK `aapt2` decoding confirms both cloud-backup and device-transfer exclusions for root, file, database, sharedpref and external domains at path `.`. SDK `apksigner` confirms the expected absence of a signature. No signing environment, key or identity was changed. Final production signing/distribution and upgrade acceptance remain separate gates.
The prior offline viewer test passed on TC22; this final packaging pass performed no device actions. No general legal clearance or claim of a fully reproduced upstream toolchain build is made. Stripped binaries limit instruction-level source attribution; the review conservatively retains identified runtime/header/startup notices instead of treating a Maven POM or missing dynamic symbol as complete evidence.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+84
View File
@@ -0,0 +1,84 @@
crtbegin_so.c
Copyright (C) 2012 The Android Open Source Project
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
============================================================
crtend_so.S
Copyright (C) 2013 The Android Open Source Project
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
============================================================
crtbrand.S
Copyright (C) 2015 The Android Open Source Project
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
+311
View File
@@ -0,0 +1,311 @@
==============================================================================
The LLVM Project is under the Apache License v2.0 with LLVM Exceptions:
==============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---- LLVM Exceptions to the Apache 2.0 License ----
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.
==============================================================================
Software from third parties included in the LLVM Project:
==============================================================================
The LLVM Project contains third party software which is under different license
terms. All such code will be identified clearly using at least one of two
mechanisms:
1) It will be in a separate directory tree with its own `LICENSE.txt` or
`LICENSE` file at the top containing the specific license and restrictions
which apply to that software, or
2) It will contain specific license and restriction terms at the top of every
file.
==============================================================================
Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy):
==============================================================================
The compiler_rt library is dual licensed under both the University of Illinois
"BSD-Like" license and the MIT license. As a user of this code you may choose
to use it under either license. As a contributor, you agree to allow your code
to be used under both.
Full text of the relevant licenses is included below.
==============================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2009-2019 by the contributors listed in CREDITS.TXT
All rights reserved.
Developed by:
LLVM Team
University of Illinois at Urbana-Champaign
http://llvm.org
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal with
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.
* Neither the names of the LLVM Team, University of Illinois at
Urbana-Champaign, nor the names of its contributors may be used to
endorse or promote products derived from this Software without specific
prior written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
SOFTWARE.
==============================================================================
Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+311
View File
@@ -0,0 +1,311 @@
==============================================================================
The LLVM Project is under the Apache License v2.0 with LLVM Exceptions:
==============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---- LLVM Exceptions to the Apache 2.0 License ----
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.
==============================================================================
Software from third parties included in the LLVM Project:
==============================================================================
The LLVM Project contains third party software which is under different license
terms. All such code will be identified clearly using at least one of two
mechanisms:
1) It will be in a separate directory tree with its own `LICENSE.txt` or
`LICENSE` file at the top containing the specific license and restrictions
which apply to that software, or
2) It will contain specific license and restriction terms at the top of every
file.
==============================================================================
Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy):
==============================================================================
The libc++ library is dual licensed under both the University of Illinois
"BSD-Like" license and the MIT license. As a user of this code you may choose
to use it under either license. As a contributor, you agree to allow your code
to be used under both.
Full text of the relevant licenses is included below.
==============================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2009-2019 by the contributors listed in CREDITS.TXT
All rights reserved.
Developed by:
LLVM Team
University of Illinois at Urbana-Champaign
http://llvm.org
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal with
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.
* Neither the names of the LLVM Team, University of Illinois at
Urbana-Champaign, nor the names of its contributors may be used to
endorse or promote products derived from this Software without specific
prior written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
SOFTWARE.
==============================================================================
Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+268
View File
@@ -0,0 +1,268 @@
{
"reviewedAt": "2026-10-31",
"verifiedApk": "scanner-host/build/outputs/apk/e2e/scanner-host-e2e.apk",
"verifiedApkSha256": "38A91781F4B4DDBD4260FEE34A5C2362D46ACB9F2E602C6582C31A9E462616AA",
"ndkRevision": "25.2.9519653",
"ndkPrebuiltRevision": "1622e087e1b881a0c7c744091d314662d12f0997",
"llvmRevision": "4c603efb0cca074e9238af8b4106c30add4418f6",
"bionicReferenceRevision": "0b981d1ec055efaef86d8a5d402cf1b3167ba10b",
"artifacts": [
{
"artifact": "graphics-path-1.0.1",
"url": "https://dl.google.com/dl/android/maven2/androidx/graphics/graphics-path/1.0.1/graphics-path-1.0.1.aar",
"aarSha256": "8CA4032B6D79B351F0B59AD4B580EDDBB9423E1652F7C958830687F1EEE2EC03",
"entry": "jni/x86_64/libandroidx.graphics.path.so",
"bytes": 10760,
"nativeSha256": "4E56C996F13670E70082658DE7880C4020EABF4F25E43387F88ED78A713FC9F0",
"apkEntry": "lib/x86_64/libandroidx.graphics.path.so",
"neededLibraries": [
"libm.so",
"libdl.so",
"libc.so"
],
"sourceRevision": "8a05a22af450d589ef911d772a001a49dcb05b71"
},
{
"artifact": "graphics-path-1.0.1",
"url": "https://dl.google.com/dl/android/maven2/androidx/graphics/graphics-path/1.0.1/graphics-path-1.0.1.aar",
"aarSha256": "8CA4032B6D79B351F0B59AD4B580EDDBB9423E1652F7C958830687F1EEE2EC03",
"entry": "jni/arm64-v8a/libandroidx.graphics.path.so",
"bytes": 10096,
"nativeSha256": "41E9A793C43A0F4FDDB19E33F346BACE464F30F888BA7B9EAF96294EA115BFB6",
"apkEntry": "lib/arm64-v8a/libandroidx.graphics.path.so",
"neededLibraries": [
"libm.so",
"libdl.so",
"libc.so"
],
"sourceRevision": "8a05a22af450d589ef911d772a001a49dcb05b71"
},
{
"artifact": "graphics-path-1.0.1",
"url": "https://dl.google.com/dl/android/maven2/androidx/graphics/graphics-path/1.0.1/graphics-path-1.0.1.aar",
"aarSha256": "8CA4032B6D79B351F0B59AD4B580EDDBB9423E1652F7C958830687F1EEE2EC03",
"entry": "jni/armeabi-v7a/libandroidx.graphics.path.so",
"bytes": 7252,
"nativeSha256": "41399EBA6FC2A60F6F14642375C1824F3CF25EB8FEC7397D753730A3CEDA3E2B",
"apkEntry": "lib/armeabi-v7a/libandroidx.graphics.path.so",
"neededLibraries": [
"libm.so",
"libdl.so",
"libc.so"
],
"sourceRevision": "8a05a22af450d589ef911d772a001a49dcb05b71"
},
{
"artifact": "graphics-path-1.0.1",
"url": "https://dl.google.com/dl/android/maven2/androidx/graphics/graphics-path/1.0.1/graphics-path-1.0.1.aar",
"aarSha256": "8CA4032B6D79B351F0B59AD4B580EDDBB9423E1652F7C958830687F1EEE2EC03",
"entry": "jni/x86/libandroidx.graphics.path.so",
"bytes": 9284,
"nativeSha256": "EB0570B41FD3BFF25D8204A967C03BD7550719E768B791F680CC40CBE35F29AF",
"apkEntry": "lib/x86/libandroidx.graphics.path.so",
"neededLibraries": [
"libm.so",
"libdl.so",
"libc.so"
],
"sourceRevision": "8a05a22af450d589ef911d772a001a49dcb05b71"
},
{
"artifact": "datastore-core-android-1.1.1",
"url": "https://dl.google.com/dl/android/maven2/androidx/datastore/datastore-core-android/1.1.1/datastore-core-android-1.1.1.aar",
"aarSha256": "883ABC911E4B14119959E44E20A520523F59C303E834D6B325007B3EE5E9CB6C",
"entry": "jni/arm64-v8a/libdatastore_shared_counter.so",
"bytes": 7112,
"nativeSha256": "D3E48717C9AA147E0AB21063BA0E8E0211CABF8BF40B222640829519EDBF58E1",
"apkEntry": "lib/arm64-v8a/libdatastore_shared_counter.so",
"neededLibraries": [
"libm.so",
"libdl.so",
"libc.so"
],
"sourceRevision": "4e91af5f5597ac6275559f2ce4bc2f5cc852bddb"
},
{
"artifact": "datastore-core-android-1.1.1",
"url": "https://dl.google.com/dl/android/maven2/androidx/datastore/datastore-core-android/1.1.1/datastore-core-android-1.1.1.aar",
"aarSha256": "883ABC911E4B14119959E44E20A520523F59C303E834D6B325007B3EE5E9CB6C",
"entry": "jni/armeabi-v7a/libdatastore_shared_counter.so",
"bytes": 4416,
"nativeSha256": "716C5D8D2CAC8CA0EDF65DA8F139C7886B726AC79D542A14EDEB94994BA6D3DC",
"apkEntry": "lib/armeabi-v7a/libdatastore_shared_counter.so",
"neededLibraries": [
"libm.so",
"libdl.so",
"libc.so"
],
"sourceRevision": "4e91af5f5597ac6275559f2ce4bc2f5cc852bddb"
},
{
"artifact": "datastore-core-android-1.1.1",
"url": "https://dl.google.com/dl/android/maven2/androidx/datastore/datastore-core-android/1.1.1/datastore-core-android-1.1.1.aar",
"aarSha256": "883ABC911E4B14119959E44E20A520523F59C303E834D6B325007B3EE5E9CB6C",
"entry": "jni/x86/libdatastore_shared_counter.so",
"bytes": 5148,
"nativeSha256": "CD4A1649E2C8350703146F94A6EE2BF4C3C8C9F172159F805B61A4A2ABDF0659",
"apkEntry": "lib/x86/libdatastore_shared_counter.so",
"neededLibraries": [
"libm.so",
"libdl.so",
"libc.so"
],
"sourceRevision": "4e91af5f5597ac6275559f2ce4bc2f5cc852bddb"
},
{
"artifact": "datastore-core-android-1.1.1",
"url": "https://dl.google.com/dl/android/maven2/androidx/datastore/datastore-core-android/1.1.1/datastore-core-android-1.1.1.aar",
"aarSha256": "883ABC911E4B14119959E44E20A520523F59C303E834D6B325007B3EE5E9CB6C",
"entry": "jni/x86_64/libdatastore_shared_counter.so",
"bytes": 6224,
"nativeSha256": "FB6C9208988C49AE94943BC3236FA763BA584722E044FA4EA9A12D2941027105",
"apkEntry": "lib/x86_64/libdatastore_shared_counter.so",
"neededLibraries": [
"libm.so",
"libdl.so",
"libc.so"
],
"sourceRevision": "4e91af5f5597ac6275559f2ce4bc2f5cc852bddb"
}
],
"sourceFiles": [
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/4e91af5f5597ac6275559f2ce4bc2f5cc852bddb/datastore/datastore-core/src/androidMain/cpp/jni/androidx_datastore_core_SharedCounter.cc",
"sha256": "ADDE5C7D79854F584E413277A03BD25BA4055C110D70C327D42F496E8FC4CE93"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/4e91af5f5597ac6275559f2ce4bc2f5cc852bddb/datastore/datastore-core/src/androidMain/cpp/shared_counter.cc",
"sha256": "DE6380BC98DF78F37DEAFF1B85B03D0309D911EA973449B2E828115145D02166"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/4e91af5f5597ac6275559f2ce4bc2f5cc852bddb/datastore/datastore-core/src/androidMain/cpp/shared_counter.h",
"sha256": "81A607D69505F760D51CC1C4477FFB6F5269EDA72811B3E9D71C7F1117A5A158"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/math/compiler.h",
"sha256": "D53E85383580E5DBF81CBCB24426B0FE1BF419B4837540242DBDCA3F1DF4CF66"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/Conic.cpp",
"sha256": "49B3DA93AA43480CF9CD481590BE48705CB9340C52E5D09635EFBD0ECE07BE83"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/Conic.h",
"sha256": "CD315AD7C8D030C72E96893E9A0FEEC463D72D2066209A203DEDCF0925398A6F"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/Path.h",
"sha256": "4EE841338063D8C6F71FF4C655ECF52559F6C6DF9AF79835388DAE1EBCD8B982"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/PathIterator.cpp",
"sha256": "3705946B43BF894E013F46CCC940F9923F43285340F8FC2128F95C59238EC7F8"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/PathIterator.h",
"sha256": "A16E99B0273FB7205607EB0CF51FC7BE518A55F3446A304F92ECAB353E77BE6F"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/pathway.cpp",
"sha256": "4BE68D6FDB6376E95560F7C49A79AA0CD97218D758CF11EA3230AC9124485800"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/scalar.h",
"sha256": "B1D2BBA80A4C9272D84473B6F09F700972B016F7579842D3A87BDBBDB2AF1EDC"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/math/TVecHelpers.h",
"sha256": "1B0D6B06C5BBD6B438F9D8440AC78CF93FB8A713761BADC59A4412B6457C1014"
},
{
"url": "https://android.googlesource.com/platform/frameworks/support/+/8a05a22af450d589ef911d772a001a49dcb05b71/graphics/graphics-path/src/main/cpp/math/vec2.h",
"sha256": "09E1A8EB3F8FDF9C1071500F1A4933F67CAEB6D0F765D5C44BA968370DE9FCB1"
}
],
"supportSources": [
{
"url": "https://android.googlesource.com/toolchain/llvm-project/+/4c603efb0cca074e9238af8b4106c30add4418f6/libcxx/LICENSE.TXT",
"sha256": "539DD7AED86E8A4F12CBDD0E6C50C189C7D74847E4FECC64CE2C6EE3A01DA38B"
},
{
"url": "https://android.googlesource.com/toolchain/llvm-project/+/4c603efb0cca074e9238af8b4106c30add4418f6/compiler-rt/LICENSE.TXT",
"sha256": "1A8F1058753F1BA890DE984E48F0242A3A5C29A6A8F2ED9FD813F36985387E8D"
},
{
"url": "https://android.googlesource.com/toolchain/prebuilts/ndk/r25/+/1622e087e1b881a0c7c744091d314662d12f0997/NOTICE",
"sha256": "31B13387E6BDE5C7E7006B63EA762020CE583BE32E4D62417584C03AC3D3F19A"
},
{
"url": "https://android.googlesource.com/toolchain/prebuilts/ndk/r25/+/1622e087e1b881a0c7c744091d314662d12f0997/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/21/crtbegin_so.o",
"sha256": "2AF3D92CFCB5A8521AD75A0F838CFAD752F7D5656566C43B0B447C972DEBED27"
}
],
"packagedArtifacts": [
{
"variant": "debug",
"apk": "scanner-host/build/outputs/apk/debug/scanner-host-debug.apk",
"apkSha256": "E12CFB7F078AD0C5F28EED717880187F394379C9532EA2D5828A228D1E4A134F",
"apkBytes": 18751646,
"noticeEntry": "res/raw/aboutlibraries.json",
"noticeSha256": "336FABEE90462B49E639247029F53260A6CD21BD5090F0F74F79A556E212552F",
"noticeEntries": 130,
"fullLicenseTexts": 9,
"nativeHashesVerified": 8,
"unknownMp3Files": 0
},
{
"variant": "e2e",
"apk": "scanner-host/build/outputs/apk/e2e/scanner-host-e2e.apk",
"apkSha256": "733285E6BE30B67A46BF2810F07376379C94E4AE529746F321262AB86CCF0740",
"apkBytes": 18810342,
"noticeEntry": "res/raw/aboutlibraries.json",
"noticeSha256": "666D3E7648DA96B26760E5D3FD41F2ABFE5B08E0E56BD5D4B0C7748A754FB474",
"noticeEntries": 130,
"fullLicenseTexts": 9,
"nativeHashesVerified": 8,
"unknownMp3Files": 0
},
{
"variant": "release",
"apk": "scanner-host/build/outputs/apk/release/scanner-host-release-unsigned.apk",
"apkSha256": "924D7EC15F93957073F0D9B47348F0E4CF7F272FBB4EBE4100FEDCBD8DB027A2",
"apkBytes": 14447558,
"noticeEntry": "res/M7.json",
"noticeSha256": "3EAA50C774B452BA89CE7516F695BDE9788088767F6A1B9A54DCC87F31394758",
"noticeEntries": 130,
"fullLicenseTexts": 9,
"nativeHashesVerified": 8,
"unknownMp3Files": 0
}
],
"packagingQualification": {
"date": "2026-10-31",
"gradleResult": "BUILD SUCCESSFUL",
"duration": "2m 32s",
"actionableTasks": 273,
"maxWorkers": 2,
"parallel": false,
"lintReleaseErrors": 0,
"lintReleaseWarnings": 0,
"releaseSigning": "unsigned; apksigner verify returns DOES NOT VERIFY / Missing META-INF/MANIFEST.MF",
"releaseDebuggable": false,
"releaseAllowBackup": false,
"releaseFullBackupContent": false,
"releaseUsesCleartextTraffic": false,
"releaseNetworkSecurityConfigOverride": false,
"releaseDataExtractionPolicy": "cloud-backup and device-transfer each exclude root/file/database/sharedpref/external at path .",
"manifestValidationTools": [
"Android SDK apkanalyzer",
"Android SDK Build Tools 35.0.0 aapt2",
"Android SDK Build Tools 35.0.0 apksigner"
]
},
"instrumentationApk": {
"apk": "scanner-host/build/outputs/apk/androidTest/e2e/scanner-host-e2e-androidTest.apk",
"sha256": "E697023F2F31F98F5F4A9A82E4A91FE8A8F5B48B08687DD784FE9960947EED35",
"bytes": 1060060
}
}
+19 -5
View File
@@ -1,8 +1,8 @@
[versions]
agp = "8.6.0"
coilCompose = "3.0.0-rc01"
coilNetworkOkhttp = "3.0.0-rc01"
coilSvg = "3.0.0-rc01"
coilCompose = "3.0.4"
coilNetworkOkhttp = "3.0.4"
coilSvg = "3.0.4"
datastorePreferences = "1.1.1"
kotlin = "1.9.0"
coreKtx = "1.13.1"
@@ -20,12 +20,25 @@ runtimeAndroid = "1.7.2"
uiAndroid = "1.6.6"
material3Android = "1.3.0"
junitJupiter = "5.8.1"
auth0 = "3.0.0-beta.0"
auth0 = "3.0.0"
uiToolingPreviewAndroid = "1.6.6"
navigationCompose = "2.8.1"
navigationCompose = "2.8.3"
okhttp = "4.12.0"
junitJunit = "4.12"
jsonTest = "20240303"
coroutinesTest = "1.9.0"
aboutLibraries = "10.9.2"
appauth = "0.11.1"
zebraEmdk = "11.0.134"
[libraries]
zebra-emdk = { module = "com.symbol:emdk", version.ref = "zebraEmdk" }
appauth = { module = "net.openid:appauth", version.ref = "appauth" }
aboutlibraries-compose = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutLibraries" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
json-test = { module = "org.json:json", version.ref = "jsonTest" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesTest" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastorePreferences" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
@@ -60,6 +73,7 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio
junit-junit = { group = "junit", name = "junit", version.ref = "junitJunit" }
[plugins]
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" }
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
Binary file not shown.
+1
View File
@@ -2,5 +2,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+26 -3
View File
@@ -1,3 +1,24 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
@@ -10,7 +31,7 @@ publishing {
repositories {
maven {
name = "githubQqqMavenRegistry"
url = uri("https://maven.pkg.github.com/Kingsrook/qqq-maven-registry")
url = uri("https://maven.pkg.github.com/QRun-IO/qqq-maven-registry")
// username and password (a personal Github access token) should be specified as
// `githubPackagesUsername` and `githubPackagesPassword` Gradle properties
credentials(PasswordCredentials::class)
@@ -51,6 +72,8 @@ dependencies {
implementation(libs.material)
testImplementation(libs.junit)
testImplementation(libs.okhttp.mockwebserver)
testImplementation("org.json:json:20240303")
testImplementation(libs.junit.jupiter)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -63,7 +86,7 @@ dependencies {
// Retrofit with Kotlin serialization Converter
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation(libs.okhttp)
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.20")
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ VERSION_NAME=0.1.1-SNAPSHOT
## POM_INCEPTION_YEAR=2024
## POM_URL=https://github.com/Kingsrook/qqq-android
## POM_LICENSE_NAME=GNU AFFERO GENERAL PUBLIC LICENSE
## POM_LICENSE_URL=https://www.gnu.org/licenses/agpl-3.0.html
## POM_LICENSE_NAME=Apache License, Version 2.0
## POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0
## POM_LICENSE_DIST=repo
## POM_SCM_URL=https://github.com/Kingsrook/qqq-android
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core
@@ -27,21 +27,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
import org.junit.Assert.assertEquals
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
/*******************************************************************************
** Instrumented test, which will execute on an Android device.
** See [testing documentation](http://d.android.com/tools/testing).
*******************************************************************************/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest
{
class ExampleInstrumentedTest {
/*******************************************************************************
**
*******************************************************************************/
@Test
fun useAppContext()
{
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.kingsrook.qqq.frontend.android.core.test", appContext.packageName)
}
}
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -29,38 +29,40 @@ import com.kingsrook.qqq.frontend.android.core.model.metadata.QProcessMetaData
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.BaseAuthenticationMetaData
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.ManageSessionRequest
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.ManageSessionResponse
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.NativeLogout
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.NativeLogoutRequest
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.NativeLogoutResponse
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.NativeLogoutData
import com.kingsrook.qqq.frontend.android.core.model.processes.ProcessStepResult
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Credentials
import okhttp3.Cookie
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.Response
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import retrofit2.Retrofit
import java.io.IOException
import java.util.TimeZone
import java.util.concurrent.ConcurrentHashMap
/***************************************************************************
**
***************************************************************************/
class NetworkQQQRepository(private var baseUrl: String) : QQQRepository
{
/*******************************************************************************
*******************************************************************************/
class NetworkQQQRepository(private var baseUrl: String, @Volatile var requestObserver: (RequestOutcome) -> Unit = {}, private val compatibility: UiApiCompatibility? = null, private val additionalHeaders: (String) -> Map<String, String> = { emptyMap() }) : QQQRepository {
private val sessionUUIDs = ConcurrentHashMap<String, String>()
private val retroJson = Json { ignoreUnknownKeys = true }
private val compatibilityByServer = ConcurrentHashMap<String, UiApiCompatibility>()
private val qqqApiServiceMap: MutableMap<String, QQQApiService> = mutableMapOf()
/***************************************************************************
**
***************************************************************************/
override fun setEnvironment(environment: Environment)
{
/*******************************************************************************
*******************************************************************************/
override fun setEnvironment(environment: Environment) {
baseUrl = environment.baseUrl
if(qqqApiServiceMap[baseUrl] == null)
{
if (qqqApiServiceMap[baseUrl] == null) {
val thisRetrofit = buildRetrofit(baseUrl)
val thisQQQApiService = thisRetrofit.create(QQQApiService::class.java)
qqqApiServiceMap[baseUrl] = thisQQQApiService
@@ -70,124 +72,280 @@ class NetworkQQQRepository(private var baseUrl: String) : QQQRepository
// retrofit.create(QQQApiService::class.java)
}
/***************************************************************************
**
***************************************************************************/
private fun buildRetrofit(baseUrl: String): Retrofit
{
/*******************************************************************************
*******************************************************************************/
private fun buildRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.addConverterFactory(retroJson.asConverterFactory("application/json".toMediaType()))
.client(
OkHttpClient.Builder()
.addInterceptor(QQQInterceptor)
.followRedirects(false)
.followSslRedirects(false)
.addInterceptor { chain ->
compatibilityByServer.getOrPut(baseUrl) {
compatibility?.takeIf { it.server == baseUrl } ?: UiApiCompatibility(baseUrl, observer = { requestObserver(it) })
}.verify()
chain.proceed(chain.request())
}
.addInterceptor(UiApiErrorInterceptor())
.addInterceptor(RequestOutcomeInterceptor { requestObserver(it) })
.addInterceptor { chain ->
val request = chain.request().newBuilder()
val deferredSession = chain.request().tag(DeferredSessionCookie::class.java) != null
if (!deferredSession) sessionUUIDs[baseUrl]?.takeIf { it.isNotBlank() }?.let { request.header("Cookie", "sessionUUID=$it") }
additionalHeaders(baseUrl).forEach { (name, value) -> request.header(name, value) }
request.header("X-QQQ-Client", ANDROID_CLIENT)
request.header("X-QQQ-UserTimezone", TimeZone.getDefault().toZoneId().toString())
val response = chain.proceed(request.build())
response.headers("Set-Cookie").forEach { header ->
Cookie.parse(response.request.url, header)?.let { cookie ->
if (!deferredSession && cookie.name == "sessionUUID" && cookie.matches(response.request.url)) {
sessionUUIDs[baseUrl] = cookie.value
}
}
}
response
}
// Last, so a caller-supplied additional header can never override the request's own origin.
.addInterceptor(SameOriginInterceptor())
.build()
)
.baseUrl(baseUrl)
.build()
}
/***************************************************************************
**
***************************************************************************/
override fun setSessionUUID(sessionUUID: String)
{
QQQInterceptor.sessionUUID = sessionUUID
/*******************************************************************************
*******************************************************************************/
override fun setSessionUUID(sessionUUID: String) {
sessionUUIDs[baseUrl] = sessionUUID
}
/***************************************************************************
**
***************************************************************************/
override suspend fun getAuthenticationMetaData(): BaseAuthenticationMetaData
{
// return qqqApiService.getAuthenticationMetaData()
/*******************************************************************************
*******************************************************************************/
override suspend fun getAuthenticationMetaData(): BaseAuthenticationMetaData {
return qqqApiServiceMap[baseUrl]!!.getAuthenticationMetaData()
}
/***************************************************************************
**
***************************************************************************/
override suspend fun manageSession(body: ManageSessionRequest): ManageSessionResponse
{
return qqqApiServiceMap[baseUrl]!!.manageSession(body)
/*******************************************************************************
*******************************************************************************/
override suspend fun manageSession(body: ManageSessionRequest): ManageSessionResponse {
val service = requireNotNull(qqqApiServiceMap[baseUrl])
return resolveSession(service.manageSession(body, DeferredSessionCookie()))
}
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
** Discover native session and nonce-validation support before starting browser authorization.
*******************************************************************************/
override suspend fun verifyNativeSessionLogin() = verifyNativeSessionLoginAt(baseUrl)
/*******************************************************************************
** Check the captured origin's capability without allowing a later server switch to redirect the exchange.
*******************************************************************************/
private suspend fun verifyNativeSessionLoginAt(server: String) = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO)
{
val version = compatibilityByServer.getOrPut(server) {
compatibility?.takeIf { it.server == server } ?: UiApiCompatibility(server, observer = { requestObserver(it) })
}.verify()
UiApiCompatibility.validateNativeSessionLogin(version)
}
/*******************************************************************************
** Return a provisional session from the initiating origin; only the current login attempt may adopt it.
*******************************************************************************/
override suspend fun exchangeNativeSession(body: ManageSessionRequest): ManageSessionResponse {
require(
body.authenticationFlow == com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.OAuth2AuthenticationMetaData.SESSION_FLOW &&
com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.isValidExpectedNonce(body.expectedNonce)
) {
"Native sign-in requires its original nonce"
}
val server = baseUrl
val service = requireNotNull(qqqApiServiceMap[server])
verifyNativeSessionLoginAt(server)
val response = service.exchangeNativeSession(body, DeferredSessionCookie())
return resolveSession(response)
}
/*******************************************************************************
** Login cookies remain provisional until the initiating ViewModel accepts the response.
*******************************************************************************/
private fun resolveSession(response: retrofit2.Response<ManageSessionResponse>): ManageSessionResponse {
if (!response.isSuccessful) throw retrofit2.HttpException(response)
val result = response.body() ?: throw IllegalStateException("Sign-in response did not establish a session")
val requestUrl = response.raw().request.url
val cookie = response.headers().values("Set-Cookie").mapNotNull { Cookie.parse(requestUrl, it) }
.lastOrNull { it.name == "sessionUUID" && it.matches(requestUrl) }?.value?.takeIf { it.isNotBlank() }
return result.copy(uuid = cookie ?: result.uuid?.takeIf { it.isNotBlank() }
?: throw IllegalStateException("Sign-in response did not establish a session"))
}
/*******************************************************************************
** Revoke an abandoned exchange at its original server without activating its response cookie.
*******************************************************************************/
override suspend fun discardNativeSession(server: String, session: String) {
val service = qqqApiServiceMap[server] ?: return
service.discardNativeSession("sessionUUID=$session", DeferredSessionCookie()).close()
}
/*******************************************************************************
** Send credentials only to the session endpoint, never to subsequent requests.
*******************************************************************************/
override suspend fun loginWithPassword(username: String, password: String): ManageSessionResponse {
val service = requireNotNull(qqqApiServiceMap[baseUrl])
return resolveSession(service.loginWithPassword(Credentials.basic(username, password, Charsets.UTF_8), emptyMap(), DeferredSessionCookie()))
}
/*******************************************************************************
** Preserve the cookie until the server has received the revocation request.
*******************************************************************************/
override suspend fun logout() {
val server = baseUrl
val session = sessionUUIDs[server]
try {
if (!session.isNullOrBlank()) discardNativeSession(server, session)
} finally {
if (session != null) sessionUUIDs.remove(server, session)
}
}
/*******************************************************************************
** Capture the original origin/cookie; neither a response cookie nor a server switch can adopt a session.
*******************************************************************************/
override suspend fun logoutWithProvider(postLogoutRedirectUri: String): NativeLogoutData? {
val server = baseUrl
val session = sessionUUIDs[server] ?: return null
try {
val supported = supportsNativeSessionLogoutAt(server)
if (!supported) {
discardNativeSession(server, session)
return null
}
return requireNotNull(qqqApiServiceMap[server]).logoutWithProvider(
"sessionUUID=$session",
NativeLogoutRequest(NativeLogout(postLogoutRedirectUri)), DeferredSessionCookie()
).use { response ->
// Parse privately: serialization errors may include input fragments, so never propagate them.
val source = response.source()
source.request(65537)
if (source.buffer.size > 65536) return@use null
try {
retroJson.decodeFromString<NativeLogoutResponse>(source.readUtf8()).nativeLogoutData
} catch (exception: Exception) {
null
}
}
} catch (exception: UiApiException) {
throw java.io.IOException("Company sign-out request failed")
} finally {
sessionUUIDs.remove(server, session)
}
}
/*******************************************************************************
** Discover optional company sign-out independently from native login support.
*******************************************************************************/
override suspend fun supportsNativeSessionLogout(): Boolean = supportsNativeSessionLogoutAt(baseUrl)
/*******************************************************************************
** Use the captured server's public capability before sending its existing session for company sign-out.
*******************************************************************************/
private suspend fun supportsNativeSessionLogoutAt(server: String): Boolean = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO)
{
try {
val version = compatibilityByServer.getOrPut(server) {
compatibility?.takeIf { it.server == server } ?: UiApiCompatibility(server, observer = { requestObserver(it) })
}.verify()
UiApiCompatibility.validateNativeSessionLogout(version)
true
} catch (exception: UiApiCompatibilityException) {
false
}
}
/*******************************************************************************
*******************************************************************************/
override suspend fun getMetaData(
frontendName: String, frontendVersion: String, applicationName: String, applicationVersion: String
): QInstance
{
): QInstance {
return qqqApiServiceMap[baseUrl]!!.getMetaData(frontendName, frontendVersion, applicationName, applicationVersion)
}
/***************************************************************************
**
***************************************************************************/
override suspend fun getProcessMetaData(processName: String): QProcessMetaData
{
/*******************************************************************************
*******************************************************************************/
override suspend fun getProcessMetaData(processName: String): QProcessMetaData {
return qqqApiServiceMap[baseUrl]!!.getProcessMetaData(processName)
}
/***************************************************************************
**
***************************************************************************/
override suspend fun processInit(processName: String, values: Map<String, Any?>, recordsParam: String?, recordIds: String?, filterJSON: String?, stepTimeoutMillis: Int?): ProcessStepResult
{
/*******************************************************************************
*******************************************************************************/
override suspend fun processInit(processName: String, values: Map<String, Any?>, recordsParam: String?, recordIds: String?, filterJSON: String?, stepTimeoutMillis: Int?): ProcessStepResult {
return qqqApiServiceMap[baseUrl]!!.processInit(processName, buildProcessInitOrStepRequestBodyV1(values, recordsParam, recordIds, filterJSON, stepTimeoutMillis))
}
/***************************************************************************
**
***************************************************************************/
override suspend fun processStep(processName: String, processUUID: String, stepName: String, values: Map<String, Any?>, stepTimeoutMillis: Int?): ProcessStepResult
{
/*******************************************************************************
*******************************************************************************/
override suspend fun processStep(processName: String, processUUID: String, stepName: String, values: Map<String, Any?>, stepTimeoutMillis: Int?): ProcessStepResult {
return qqqApiServiceMap[baseUrl]!!.processStep(processName, processUUID, stepName, buildProcessInitOrStepRequestBodyV1(values, stepTimeoutMillis = stepTimeoutMillis))
}
/***************************************************************************
**
***************************************************************************/
override suspend fun processJobStatus(processName: String, processUUID: String, jobUUID: String): ProcessStepResult
{
/*******************************************************************************
*******************************************************************************/
override suspend fun processCancel(processName: String, processUUID: String) {
qqqApiServiceMap[baseUrl]!!.processCancel(processName, processUUID).close()
}
/*******************************************************************************
** Options use the semantic `{options:[{id,label}]}` response; ids stay JSON scalars so they round-trip unchanged.
*******************************************************************************/
override suspend fun processPossibleValues(processName: String, fieldName: String, values: Map<String, Any?>, processValues: Map<String, Any?>): List<com.kingsrook.qqq.frontend.android.core.model.QPossibleValue> {
fun json(map: Map<String, Any?>) = JSONObject().apply { map.forEach { (key, value) -> put(key, value ?: JSONObject.NULL) } }
val request = JSONObject().put("values", json(values)).put("processValues", json(processValues)).put("useCase", "form")
val body = qqqApiServiceMap[baseUrl]!!.processPossibleValues(
processName, fieldName, request.toString().toRequestBody("application/json".toMediaType())
).use { JSONObject(it.string()) }
// The server omits `options` when empty.
val options = body.optJSONArray("options") ?: return emptyList()
return (0 until options.length()).map { options.getJSONObject(it) }.map { option ->
val id = option.get("id")
com.kingsrook.qqq.frontend.android.core.model.QPossibleValue(
id as? java.io.Serializable ?: id.toString(), option.optString("label", id.toString()))
}
}
/*******************************************************************************
** Poll the exact process/job identity supplied by the server instead of assuming background completion.
*******************************************************************************/
override suspend fun processJobStatus(processName: String, processUUID: String, jobUUID: String): ProcessStepResult {
return qqqApiServiceMap[baseUrl]!!.processJobStatus(processName, processUUID, jobUUID)
}
/***************************************************************************
**
***************************************************************************/
private fun buildProcessInitOrStepRequestBodyV1(values: Map<String, Any?>, recordsParam: String? = null, recordIds: String? = null, filterJSON: String? = null, stepTimeoutMillis: Int? = null): RequestBody
{
/*******************************************************************************
*******************************************************************************/
private fun buildProcessInitOrStepRequestBodyV1(values: Map<String, Any?>, recordsParam: String? = null, recordIds: String? = null, filterJSON: String? = null, stepTimeoutMillis: Int? = null): RequestBody {
val bodyBuilder = MultipartBody.Builder()
.setType(MultipartBody.FORM)
if(!values.isEmpty())
{
if (!values.isEmpty()) {
val valuesJson = JSONObject()
for(entry in values.entries)
{
valuesJson.put(entry.key, entry.value)
for (entry in values.entries) {
valuesJson.put(entry.key, entry.value ?: JSONObject.NULL)
}
bodyBuilder.addFormDataPart("values", valuesJson.toString())
}
if(recordsParam != null)
{
if (recordsParam != null) {
bodyBuilder.addFormDataPart("recordsParam", recordsParam)
}
if(recordIds != null)
{
if (recordIds != null) {
bodyBuilder.addFormDataPart("recordIds", recordIds)
}
if(filterJSON != null)
{
if (filterJSON != null) {
bodyBuilder.addFormDataPart("filterJSON", filterJSON)
}
if(stepTimeoutMillis != null)
{
if (stepTimeoutMillis != null) {
bodyBuilder.addFormDataPart("stepTimeoutMillis", stepTimeoutMillis.toString())
}
@@ -195,46 +353,18 @@ class NetworkQQQRepository(private var baseUrl: String) : QQQRepository
return requestBody
}
/***************************************************************************
**
***************************************************************************/
override suspend fun resource(path: String): ByteArray?
{
return qqqApiServiceMap[baseUrl]!!.resource(path)
}
/*******************************************************************************
*******************************************************************************/
/***************************************************************************
**
***************************************************************************/
override fun getURI(path: String): String
{
return "${baseUrl.replaceFirst(Regex("/*$"), "/")}${path.replaceFirst(Regex("^/+"), "")}"
}
/***************************************************************************
**
***************************************************************************/
companion object QQQInterceptor : Interceptor
{
private var sessionUUID: String? = null
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response
{
val requestBuilder = chain.request().newBuilder()
sessionUUID?.let()
{
requestBuilder.addHeader("Cookie", "sessionUUID=${it}")
}
val timeZone = TimeZone.getDefault()
requestBuilder.addHeader("X-QQQ-UserTimezone", timeZone.toZoneId().toString())
val response: Response = chain.proceed(requestBuilder.build())
return response
}
/*******************************************************************************
*******************************************************************************/
override fun getURI(path: String): String {
val uri = java.net.URI(baseUrl).resolve(path)
require(uri.scheme in setOf("https", "http") && uri.userInfo == null && uri.host != null) { "Unsupported media URL" }
return uri.toASCIIString()
}
/*******************************************************************************
*******************************************************************************/
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -29,32 +29,94 @@ import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.Man
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.ManageSessionResponse
import com.kingsrook.qqq.frontend.android.core.model.processes.ProcessStepResult
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
const val BASE_PATH = "/qqq/v1/"
/***************************************************************************
/*******************************************************************************
** A login response is adopted only after its initiating ViewModel accepts it.
*******************************************************************************/
class DeferredSessionCookie
/*******************************************************************************
** Class (interface?) that instructs retrofit how to make HTTP requests
** to a QQQ middleware server.
***************************************************************************/
interface QQQApiService
{
*******************************************************************************/
interface QQQApiService {
//////////////////////////////
// authentication endpoints //
//////////////////////////////
/*******************************************************************************
** Discover the versioned server authentication contract before submitting credentials.
*******************************************************************************/
@GET(BASE_PATH + "metaData/authentication")
suspend fun getAuthenticationMetaData(): BaseAuthenticationMetaData
/*******************************************************************************
** Exchange authentication data for a provisional QQQ session without adopting a cookie implicitly.
*******************************************************************************/
@POST(BASE_PATH + "manageSession")
suspend fun manageSession(@Body body: ManageSessionRequest): ManageSessionResponse
suspend fun manageSession(
@Body body: ManageSessionRequest,
@retrofit2.http.Tag cookiePolicy: DeferredSessionCookie
): retrofit2.Response<ManageSessionResponse>
/*******************************************************************************
** Send the native authorization exchange only to the versioned session endpoint.
*******************************************************************************/
@POST(BASE_PATH + "manageSession")
suspend fun exchangeNativeSession(
@Body body: ManageSessionRequest,
@retrofit2.http.Tag cookiePolicy: DeferredSessionCookie
): retrofit2.Response<ManageSessionResponse>
/*******************************************************************************
** Keep password credentials confined to the session endpoint rather than ordinary process requests.
*******************************************************************************/
@POST(BASE_PATH + "manageSession")
suspend fun loginWithPassword(
@Header("Authorization") authorization: String, @Body body: Map<String, String>,
@retrofit2.http.Tag cookiePolicy: DeferredSessionCookie
): retrofit2.Response<ManageSessionResponse>
/*******************************************************************************
** Revoke the supplied QQQ session before its caller discards local credentials.
*******************************************************************************/
@POST(BASE_PATH + "logout")
suspend fun logout(): ResponseBody
/*******************************************************************************
** Request optional company sign-out using the existing session and a registered callback.
*******************************************************************************/
@POST(BASE_PATH + "logout")
@retrofit2.http.Streaming
suspend fun logoutWithProvider(
@Header("Cookie") cookie: String,
@Body body: com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.NativeLogoutRequest,
@retrofit2.http.Tag cookiePolicy: DeferredSessionCookie
): ResponseBody
/*******************************************************************************
** Revoke a captured abandoned session without installing its cookie in the active repository.
*******************************************************************************/
@POST(BASE_PATH + "logout")
suspend fun discardNativeSession(
@Header("Cookie") cookie: String,
@retrofit2.http.Tag cookiePolicy: DeferredSessionCookie
): ResponseBody
///////////////////
// qqq meta data //
///////////////////
/*******************************************************************************
** Load semantic metadata with explicit frontend and application versions for compatibility decisions.
*******************************************************************************/
@GET(BASE_PATH + "metaData")
suspend fun getMetaData(
@Query("frontendName") frontendName: String,
@@ -66,15 +128,24 @@ interface QQQApiService
///////////////
// processes //
///////////////
/*******************************************************************************
** Read the server-defined process contract rather than deriving behavior from its name.
*******************************************************************************/
@GET(BASE_PATH + "metaData/process/{processName}")
suspend fun getProcessMetaData(@Path(value = "processName") processName: String): QProcessMetaData
/*******************************************************************************
** Start process execution with explicit record selection and values, returning authoritative server step state.
*******************************************************************************/
@POST(BASE_PATH + "processes/{processName}/init")
suspend fun processInit(
@Path(value = "processName") processName: String,
@Body body: RequestBody
): ProcessStepResult
/*******************************************************************************
** Submit values to the identified server process instance and step.
*******************************************************************************/
@POST(BASE_PATH + "processes/{processName}/{processUUID}/step/{stepName}")
suspend fun processStep(
@Path(value = "processName") processName: String,
@@ -83,6 +154,9 @@ interface QQQApiService
@Body body: RequestBody
): ProcessStepResult
/*******************************************************************************
** Poll the identified server job within its owning process instance.
*******************************************************************************/
@GET(BASE_PATH + "processes/{processName}/{processUUID}/status/{jobUUID}")
suspend fun processJobStatus(
@Path(value = "processName") processName: String,
@@ -90,8 +164,22 @@ interface QQQApiService
@Path(value = "jobUUID") jobUUID: String,
): ProcessStepResult
@GET(BASE_PATH + "{path}")
suspend fun resource(@Path(value = "path") path: String): ByteArray?
/*******************************************************************************
** Published operation `searchProcessPossibleValues`: JSON body {values, processValues, useCase, searchTerm, ids}.
*******************************************************************************/
@POST(BASE_PATH + "processes/{processName}/possibleValues/{fieldName}")
suspend fun processPossibleValues(
@Path(value = "processName") processName: String,
@Path(value = "fieldName") fieldName: String,
@Body body: RequestBody
): ResponseBody
/*******************************************************************************
** Cancel the identified server process explicitly instead of dropping only client navigation.
*******************************************************************************/
@POST(BASE_PATH + "processes/{processName}/{processUUID}/cancel")
suspend fun processCancel(@Path("processName") processName: String, @Path("processUUID") processUUID: String): ResponseBody
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -30,31 +30,115 @@ import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.Man
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.ManageSessionResponse
import com.kingsrook.qqq.frontend.android.core.model.processes.ProcessStepResult
/***************************************************************************
/*******************************************************************************
** interface that defines the interaction points we have with a QQQ backend.
***************************************************************************/
interface QQQRepository
{
*******************************************************************************/
interface QQQRepository {
/*******************************************************************************
** Select the server environment before requests; implementations must isolate session state by server.
*******************************************************************************/
fun setEnvironment(environment: Environment)
/*******************************************************************************
** Activate only a session accepted by the current login attempt, never an abandoned provisional response.
*******************************************************************************/
fun setSessionUUID(sessionUUID: String)
/*******************************************************************************
** Discover the server's authentication contract without submitting credentials.
*******************************************************************************/
suspend fun getAuthenticationMetaData(): BaseAuthenticationMetaData
/*******************************************************************************
** Return a provisional session; the initiating login must accept it before calling setSessionUUID.
*******************************************************************************/
suspend fun manageSession(body: ManageSessionRequest): ManageSessionResponse
/*******************************************************************************
** Native browser sign-in requires explicit version/feature discovery before authorization.
*******************************************************************************/
suspend fun verifyNativeSessionLogin() {
throw UnsupportedOperationException("Native browser sign-in is unavailable")
}
/*******************************************************************************
** Return the new session without activating it; the caller must validate its pending login first.
*******************************************************************************/
suspend fun exchangeNativeSession(body: ManageSessionRequest): ManageSessionResponse =
throw UnsupportedOperationException("Native browser sign-in is unavailable")
/*******************************************************************************
** Revoke an abandoned exchange at its original server without activating its cookie.
*******************************************************************************/
suspend fun discardNativeSession(server: String, session: String) {}
/*******************************************************************************
** Authenticate without retaining the password or adopting the provisional response cookie.
*******************************************************************************/
suspend fun loginWithPassword(username: String, password: String): ManageSessionResponse =
throw UnsupportedOperationException("Password authentication is unavailable")
/*******************************************************************************
** End the server session before discarding local credentials.
*******************************************************************************/
suspend fun logout() {}
/*******************************************************************************
** Public discovery only. Absence retains ordinary local sign-out.
*******************************************************************************/
suspend fun supportsNativeSessionLogout(): Boolean = false
/*******************************************************************************
** Explicit optional company sign-out. Unsupported repositories still perform ordinary logout.
*******************************************************************************/
suspend fun logoutWithProvider(postLogoutRedirectUri: String): com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.NativeLogoutData? {
logout()
return null
}
/*******************************************************************************
** Load the semantic application model with explicit client identity for server compatibility decisions.
*******************************************************************************/
suspend fun getMetaData(frontendName: String, frontendVersion: String, applicationName: String, applicationVersion: String): QInstance
/*******************************************************************************
** Resolve the server-defined process contract instead of deriving actions from its name.
*******************************************************************************/
suspend fun getProcessMetaData(processName: String): QProcessMetaData
/*******************************************************************************
** Start server-defined process execution with explicit values and record selection; return authoritative step state.
*******************************************************************************/
suspend fun processInit(processName: String, values: Map<String, Any?>, recordsParam: String? = null, recordIds: String? = null, filterJSON: String? = null, stepTimeoutMillis: Int? = 3000): ProcessStepResult
/*******************************************************************************
** Submit to the existing process instance and server-named step, preserving the caller's timeout policy.
*******************************************************************************/
suspend fun processStep(processName: String, processUUID: String, stepName: String, values: Map<String, Any?>, stepTimeoutMillis: Int?): ProcessStepResult
/*******************************************************************************
** Cancel the existing server process explicitly; unsupported implementations must fail visibly.
*******************************************************************************/
suspend fun processCancel(processName: String, processUUID: String) {
throw UnsupportedOperationException("Process cancellation is unavailable")
}
/*******************************************************************************
** Search a process field's server possible-value source (`searchProcessPossibleValues`) with the current form values
** (`values`, used for dependsOn/filter inputs) and scalar process values, using the FORM use case.
*******************************************************************************/
suspend fun processPossibleValues(processName: String, fieldName: String, values: Map<String, Any?>, processValues: Map<String, Any?> = emptyMap()): List<com.kingsrook.qqq.frontend.android.core.model.QPossibleValue> =
throw UnsupportedOperationException("Remote possible values are unavailable")
/*******************************************************************************
** Poll the server-issued job within its process instance instead of inferring asynchronous completion.
*******************************************************************************/
suspend fun processJobStatus(processName: String, processUUID: String, jobUUID: String): ProcessStepResult
suspend fun resource(path: String): ByteArray?
/*******************************************************************************
** Resolve a repository resource path against the currently selected server environment.
*******************************************************************************/
fun getURI(path: String): String
}
@@ -0,0 +1,36 @@
/* Copyright (c) 2026 QRunIO. SPDX-License-Identifier: Apache-2.0 */
package com.kingsrook.qqq.frontend.android.core.controllers
import java.io.IOException
import okhttp3.Interceptor
import okhttp3.Response
/*******************************************************************************
** Deliberately excludes URLs, headers, bodies, and exception messages. Null status means transport failure.
*******************************************************************************/
data class RequestOutcome(val httpStatus: Int?, val elapsedMillis: Long)
/*******************************************************************************
** Optional diagnostics must never change request behavior, even if an observer throws.
*******************************************************************************/
class RequestOutcomeInterceptor(private val observer: (RequestOutcome) -> Unit) : Interceptor {
/*******************************************************************************
** Observe status and monotonic duration without retaining sensitive request data or changing HTTP behavior.
*******************************************************************************/
override fun intercept(chain: Interceptor.Chain): Response {
val started = System.nanoTime()
/*******************************************************************************
** Isolate observer failures so optional diagnostics cannot fail the original request.
*******************************************************************************/
fun report(status: Int?) {
runCatching { observer(RequestOutcome(status, (System.nanoTime() - started) / 1_000_000)) }
}
try {
return chain.proceed(chain.request()).also { report(it.code) }
} catch (exception: IOException) {
report(null)
throw exception
}
}
}
@@ -0,0 +1,31 @@
/* Copyright (c) 2026 QRunIO. SPDX-License-Identifier: Apache-2.0 */
package com.kingsrook.qqq.frontend.android.core.controllers
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
/*******************************************************************************
** Native requests have no browser to supply `Origin`, yet cookie-bearing QQQ mutations must present their own origin.
** The value is always the canonical scheme/host/port of the actual request URL: never credentials, input or header state.
** Install it only on clients that talk to the selected QQQ server, never on provider, logo or download clients, and
** after any header-adding interceptor so that it has the final say.
*******************************************************************************/
class SameOriginInterceptor : Interceptor {
/*******************************************************************************
** Replace any existing `Origin` with the origin of the URL being requested; redirects stay refused by the clients.
*******************************************************************************/
override fun intercept(chain: Interceptor.Chain): Response =
chain.proceed(chain.request().newBuilder().header("Origin", httpOrigin(chain.request().url)).build())
companion object {
/*******************************************************************************
** `scheme://host[:port]` with the scheme's default port omitted and IPv6 hosts bracketed.
*******************************************************************************/
fun httpOrigin(url: HttpUrl): String {
val host = if (url.host.contains(':')) "[${url.host}]" else url.host
val port = if (url.port == HttpUrl.defaultPort(url.scheme)) "" else ":${url.port}"
return "${url.scheme}://$host$port"
}
}
}
@@ -0,0 +1,128 @@
/* Copyright (c) 2026 QRunIO. SPDX-License-Identifier: Apache-2.0 */
package com.kingsrook.qqq.frontend.android.core.controllers
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.Request
const val ANDROID_CLIENT = "android/qqq-mobile"
const val MOBILE_SCANNERS_PATH = "/qqq/v1/qbit/mobile-scanners"
/*******************************************************************************
** Carry the named extension's version and registered path prefix for pre-credential compatibility checks.
*******************************************************************************/
@Serializable
data class UiApiExtension(val name: String, val version: String, val pathPrefix: String)
/*******************************************************************************
** Represent public server capabilities independently of any authenticated session.
*******************************************************************************/
@Serializable
data class UiApiVersion(
val apiVersion: String, val features: List<String> = emptyList(), val extensions: List<UiApiExtension> = emptyList(),
val clientChannels: List<String> = emptyList()
)
/*******************************************************************************
** Stop unsupported client/server combinations with a safe operator-facing explanation.
*******************************************************************************/
class UiApiCompatibilityException(message: String) : IOException(message)
/*******************************************************************************
** One origin and one host lifetime. Discovery uses a separate client with no credentials or cookies.
*******************************************************************************/
class UiApiCompatibility(
val server: String, private val requireMobileScanners: Boolean = false,
private val observer: (RequestOutcome) -> Unit = {}
) {
private val client = OkHttpClient.Builder().followRedirects(false).followSslRedirects(false)
.callTimeout(5, TimeUnit.SECONDS).addInterceptor(RequestOutcomeInterceptor(observer)).build()
@Volatile
private var verified: UiApiVersion? = null
/*******************************************************************************
** Non-blocking view of an already verified result; never performs discovery, so it is safe on the UI thread.
*******************************************************************************/
fun verifiedVersion(): UiApiVersion? = verified
/*******************************************************************************
** Call on an IO thread. Synchronization prevents competing startup requests bypassing discovery.
*******************************************************************************/
@Synchronized
fun verify(): UiApiVersion {
verified?.let { return it }
val request = Request.Builder().url(server.trimEnd('/') + "/qqq/v1/version")
.header("X-QQQ-Client", ANDROID_CLIENT).header("Cache-Control", "no-store").build()
val version = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw UiApiCompatibilityException("This server does not provide a supported QQQ UI API. Ask your administrator to upgrade the server.")
val source = response.body?.source()
source?.request(65537)
val bytes = source?.buffer?.readByteArray(minOf(source.buffer.size, 65537)) ?: byteArrayOf()
if (bytes.size > 65536) throw UiApiCompatibilityException("The server returned invalid QQQ UI API version information.")
try {
Json { ignoreUnknownKeys = true; coerceInputValues = true }.decodeFromString<UiApiVersion>(bytes.toString(Charsets.UTF_8))
} catch (exception: Exception) {
throw UiApiCompatibilityException("The server returned invalid QQQ UI API version information.")
}
}
validate(version, requireMobileScanners)
verified = version
return version
}
/*******************************************************************************
** Keep supported protocol ranges and feature checks shared by startup and optional authentication discovery.
*******************************************************************************/
companion object {
// Compiled and tested against the published 1.8.0 contract; additive 1.x versions remain compatible.
const val SUPPORTED_CORE_RANGE = ">=1.8.0 <2.0.0"
const val SUPPORTED_MOBILE_RANGE = ">=1.1.0 <2.0.0"
val REQUIRED_FEATURES = setOf("version", "metaData", "processes", "processCancel", "extensions", "widgets")
/*******************************************************************************
** Require the original-nonce native session contract before opening the authorization browser.
*******************************************************************************/
fun validateNativeSessionLogin(version: UiApiVersion) {
val parts = Regex("1\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)").matchEntire(version.apiVersion)?.groupValues
if (parts == null || (parts[1].toIntOrNull() ?: 0) < 10 ||
!version.features.containsAll(listOf("nativeSessionLogin", "nativeSessionNonceValidation"))
)
throw UiApiCompatibilityException("This server does not advertise native browser sign-in with required nonce validation. Ask your administrator to enable the supported login flow.")
}
/*******************************************************************************
** The logout capability is additive and must never be inferred from native login support.
*******************************************************************************/
fun validateNativeSessionLogout(version: UiApiVersion) {
val parts = Regex("1\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)").matchEntire(version.apiVersion)?.groupValues
if (parts == null || (parts[1].toIntOrNull() ?: 0) < 10 || "nativeSessionLogout" !in version.features)
throw UiApiCompatibilityException("This server does not advertise company sign-out.")
}
/*******************************************************************************
** Require compatible API semantics, Android channel and optional scanner extension before credentials are sent.
*******************************************************************************/
fun validate(version: UiApiVersion, requireMobileScanners: Boolean) {
/*******************************************************************************
** Accept additive versions in the supported major while rejecting malformed or overflowing version components.
*******************************************************************************/
fun supported(value: String, minMinor: Int): Boolean {
val parts = Regex("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)").matchEntire(value)?.groupValues ?: return false
return parts[1] == "1" && (parts[2].toIntOrNull() ?: return false) >= minMinor && parts[3].toIntOrNull() != null
}
if (!supported(version.apiVersion, 8)) throw UiApiCompatibilityException("Unsupported QQQ UI API version. This app requires $SUPPORTED_CORE_RANGE. Update the app or ask your administrator to upgrade the server.")
if (!version.features.containsAll(REQUIRED_FEATURES)) throw UiApiCompatibilityException("This server is missing required QQQ UI API features. Ask your administrator to upgrade the server.")
if ("android" !in version.clientChannels) throw UiApiCompatibilityException("This server does not support the Android client channel. Ask your administrator to enable the android channel.")
if (requireMobileScanners) {
val extension = version.extensions.singleOrNull { it.name == "mobile-scanners" }
?: throw UiApiCompatibilityException("This server is missing the mobile-scanners extension. Ask your administrator to install it.")
if (!supported(extension.version, 1) || extension.pathPrefix != "/qbit/mobile-scanners")
throw UiApiCompatibilityException("Unsupported mobile-scanners extension. This app requires $SUPPORTED_MOBILE_RANGE at /qbit/mobile-scanners. Update the app or server extension.")
}
}
}
}
@@ -0,0 +1,67 @@
/* Copyright (c) 2026 QRunIO. SPDX-License-Identifier: Apache-2.0 */
package com.kingsrook.qqq.frontend.android.core.controllers
import java.io.IOException
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Response
/*******************************************************************************
** Associate a server validation message with its semantic field without adding it to diagnostic telemetry.
*******************************************************************************/
@Serializable
data class UiApiFieldError(val field: String = "", val message: String = "", val code: String? = null)
/*******************************************************************************
** Decode the bounded semantic error envelope while tolerating additive server fields.
*******************************************************************************/
@Serializable
private data class UiApiErrorBody(
val message: String? = null, val code: String? = null,
val fieldErrors: List<UiApiFieldError> = emptyList(), val traceId: String? = null
)
/*******************************************************************************
** Human-facing server errors are available to the UI, never to the diagnostic event log.
*******************************************************************************/
class UiApiException(
val httpStatus: Int, val code: String, message: String,
val fieldErrors: List<UiApiFieldError> = emptyList(), val traceId: String? = null
) : IOException(message) {
/*******************************************************************************
** Centralize bounded response parsing and safe fallback messages for HTTP errors.
*******************************************************************************/
companion object {
/*******************************************************************************
** Peek at a bounded error body without consuming response ownership and retain a safe HTTP fallback.
*******************************************************************************/
fun from(response: Response): UiApiException {
val body = runCatching { Json { ignoreUnknownKeys = true }.decodeFromString<UiApiErrorBody>(response.peekBody(65536).string()) }.getOrNull()
val fallback = when (response.code) {
400 -> "BAD_REQUEST"; 401 -> "UNAUTHENTICATED"; 403 -> "PERMISSION_DENIED"
404 -> "NOT_FOUND"; 409 -> "CONFLICT"; 429 -> "RATE_LIMITED"; 500 -> "INTERNAL_ERROR"; else -> "ERROR"
}
return UiApiException(
response.code, body?.code ?: fallback,
body?.message?.takeIf { it.isNotBlank() } ?: "The server could not complete this request (HTTP ${response.code}).",
body?.fieldErrors ?: emptyList(), body?.traceId ?: response.header("X-QQQ-Trace-Id"))
}
}
}
/*******************************************************************************
** Convert failed responses into semantic exceptions while closing their network resources.
*******************************************************************************/
class UiApiErrorInterceptor : Interceptor {
/*******************************************************************************
** Return successful response ownership to the caller and close failed responses before throwing.
*******************************************************************************/
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (!response.isSuccessful) {
val error = UiApiException.from(response); response.close(); throw error
}
return response
}
}
@@ -1,35 +1,33 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class Environment(
val label: String,
val baseUrl: String,
)
{
}
) {
}
@@ -1,357 +1,202 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.kingsrook.qqq.frontend.android.core.model
import kotlinx.serialization.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.longOrNull
import kotlinx.serialization.serializer
import kotlin.reflect.full.starProjectedType
/***************************************************************************
** Credit for much of this file - it originally came from:
** https://gist.github.com/3v1n0/ecbc5e825e2921bd0022611d7046690b
***************************************************************************/
typealias MapOfStringAny = Map<String, @Serializable(with = AnyValueSerializer::class) Any?>
typealias MutableMapOfStringAny = MutableMap<String, @Serializable(with = AnyValueSerializer::class) Any?>
typealias ListOfAny = List<@Serializable(with = AnyValueSerializer::class) Any?>
/*******************************************************************************
** Legacy public model retained for source compatibility; it has no special meaning in API JSON.
*******************************************************************************/
@Serializable
data class AnyValueSurrogate(
val type: String,
@Contextual
val value: Any?
)
data class AnyValueSurrogate(val type: String, @Contextual val value: Any?)
/*******************************************************************************
** Legacy null marker retained for callers; API nulls use JSON null.
*******************************************************************************/
@Serializable
object NoneType
/***************************************************************************
**
***************************************************************************/
object AnyValueSerializer : KSerializer<Any?>
{
override val descriptor: SerialDescriptor = AnyValueSurrogate.serializer().descriptor
/***************************************************************************
**
***************************************************************************/
override fun serialize(encoder: Encoder, value: Any?)
{
if(value != null)
{
val valueClass = value::class
val valueType = valueClass.starProjectedType
val valueSerializer = serializer(valueType)
if(encoder is JsonEncoder && isTypePrimitive(valueType))
{
encoder.encodeJsonElement(Json.encodeToJsonElement(valueSerializer, value))
}
else
{
/////////////////////////////////////////////////////////////////
// Would be nice to use valueSerializer.descriptor.serialName, //
// but how to deserialize that to a type? //
/////////////////////////////////////////////////////////////////
val composite = encoder.beginCollection(descriptor, 2)
composite.encodeSerializableElement(descriptor, 0, serializer(), valueClass.java.name)
composite.encodeSerializableElement(descriptor, 1, valueSerializer, value)
composite.endStructure(descriptor)
}
}
else
{
if(encoder is JsonEncoder)
{
encoder.encodeJsonElement(JsonNull)
}
else
{
val composite = encoder.beginCollection(descriptor, 2)
composite.encodeSerializableElement(descriptor, 1, serializer<NoneType?>(), null)
composite.endStructure(descriptor)
}
}
}
/***************************************************************************
**
***************************************************************************/
private fun isTypePrimitive(type: KType): Boolean
{
////////////////////////////////////////////////////////////////////////////////////////////////
// This can be replaced when using experimental API (via @ExperimentalSerializationApi) with: //
// valueSerializer.descriptor.kind is PrimitiveKind //
////////////////////////////////////////////////////////////////////////////////////////////////
if(type.isSubtypeOf(Number::class.starProjectedType))
{
return true
}
if(type.isSubtypeOf(String::class.starProjectedType))
{
return true
}
if(type.isSubtypeOf(Boolean::class.starProjectedType))
{
return true
}
return false
}
/***************************************************************************
**
***************************************************************************/
private fun getSerializerForTypeName(strType: String): KSerializer<*>
{
return try
{
serializer(Class.forName(strType).kotlin.starProjectedType)
}
catch(e: ClassNotFoundException)
{
throw SerializationException(e.message)
}
}
/***************************************************************************
**
***************************************************************************/
override fun deserialize(decoder: Decoder): Any?
{
if(decoder is JsonDecoder)
{
val element = decoder.decodeJsonElement()
if(element is JsonNull)
{
return null
}
if(element is JsonPrimitive)
{
if(element.isString)
{
return element.content
}
return try
{
element.boolean
}
catch(e: Throwable)
{
try
{
element.long
}
catch(e: Throwable)
{
element.double
}
}
}
else if(element is JsonObject && "type" in element && "value" in element)
{
element["type"].also()
{ type ->
if(type is JsonPrimitive && type.isString)
{
val valueSerializer = getSerializerForTypeName(type.content)
element["value"].also()
{ value ->
if(value is JsonObject)
{
return Json.decodeFromJsonElement(valueSerializer, value)
}
}
}
}
}
else if(element is JsonObject)
{
val decodedMap = Json.decodeFromString<MapStringAny>(element.toString())
return (decodedMap.entries)
}
else if(element is JsonArray)
{
val decodedList = Json.decodeFromString<ListAny>(element.toString())
return (decodedList.entries)
}
throw SerializationException("Invalid Json element $element")
}
else
{
val composite = decoder.beginStructure(descriptor)
var index = composite.decodeElementIndex(descriptor)
if(index == CompositeDecoder.DECODE_DONE)
{
return null
}
val strType = composite.decodeStringElement(descriptor, index)
if(strType.isEmpty())
{
throw SerializationException("Unknown serialization type")
}
index = composite.decodeElementIndex(descriptor).also()
{
if(it != index + 1)
{
throw SerializationException("Unexpected element index!")
}
}
getSerializerForTypeName(strType).also()
{ serializer ->
composite.decodeSerializableElement(descriptor, index, serializer).also()
{
composite.endStructure(descriptor)
return it
}
}
}
}
}
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
** Wrapper retained for clients using the original generic serializer API.
*******************************************************************************/
@Serializable(with = AnySerializableValueSerializer::class)
data class AnySerializableValue(val value: Any?)
/***************************************************************************
**
***************************************************************************/
object AnySerializableValueSerializer : KSerializer<AnySerializableValue>
{
override val descriptor: SerialDescriptor = AnyValueSurrogate.serializer().descriptor
override fun serialize(encoder: Encoder, value: AnySerializableValue) =
AnyValueSerializer.serialize(encoder, value.value)
/*******************************************************************************
** Delegate wrapped values to the JSON data serializer without class discovery.
*******************************************************************************/
object AnySerializableValueSerializer : KSerializer<AnySerializableValue> {
override val descriptor = JsonElement.serializer().descriptor
override fun deserialize(decoder: Decoder): AnySerializableValue =
AnySerializableValue(AnyValueSerializer.deserialize(decoder))
/*******************************************************************************
** Delegate framing to the declared JSON serializer without introducing wire-controlled JVM class discovery.
*******************************************************************************/
override fun serialize(encoder: Encoder, value: AnySerializableValue) = AnyValueSerializer.serialize(encoder, value.value)
/*******************************************************************************
** Reconstruct the declared wrapper through its JSON delegate instead of resolving classes from input.
*******************************************************************************/
override fun deserialize(decoder: Decoder) = AnySerializableValue(AnyValueSerializer.deserialize(decoder))
}
/***************************************************************************
**
***************************************************************************/
typealias SerializableMapType = MutableMap<String, AnySerializableValue>
typealias SerializableListType = MutableList<AnySerializableValue>
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
** Legacy collection declarations retain their original public API shape.
*******************************************************************************/
@Serializable
abstract class SerializableMap : SerializableMapType
/***************************************************************************
**
***************************************************************************/
object MapStringAnySerializer : KSerializer<MapStringAny>
{
override val descriptor: SerialDescriptor = SerializableMap.serializer().descriptor
/***************************************************************************
**
***************************************************************************/
override fun serialize(encoder: Encoder, value: MapStringAny)
{
val entries: SerializableMapType = mutableMapOf()
value.entries.forEach { entries[it.key] = AnySerializableValue(it.value) }
encoder.encodeSerializableValue(serializer(), entries)
}
/***************************************************************************
**
***************************************************************************/
override fun deserialize(decoder: Decoder): MapStringAny
{
val map = mutableMapOf<String, Any?>()
decoder.decodeSerializableValue(serializer<SerializableMapType>()).forEach {
map[it.key] = it.value.value
}
return MapStringAny(map)
}
}
/***************************************************************************
**
***************************************************************************/
@Serializable(with = MapStringAnySerializer::class)
data class MapStringAny(val entries: Map<String, Any?> = emptyMap())
/***************************************************************************
**
***************************************************************************/
typealias SerializableListType = MutableList<AnySerializableValue>
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
** Legacy collection declarations retain their original public API shape.
*******************************************************************************/
@Serializable
abstract class SerializableList : SerializableListType
/***************************************************************************
**
***************************************************************************/
object ListAnySerializer : KSerializer<ListAny>
{
override val descriptor: SerialDescriptor = SerializableList.serializer().descriptor
/*******************************************************************************
** Semantic API values are JSON data; business keys never select JVM classes.
*******************************************************************************/
object AnyValueSerializer : KSerializer<Any?> {
override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor
/***************************************************************************
**
***************************************************************************/
override fun serialize(encoder: Encoder, value: ListAny)
{
val entries: SerializableListType = mutableListOf()
value.entries.forEach { entries.add(AnySerializableValue(it)) }
encoder.encodeSerializableValue(serializer(), entries)
/*******************************************************************************
** Encode arbitrary API maps/lists without adding class-name envelopes.
*******************************************************************************/
override fun serialize(encoder: Encoder, value: Any?) {
val json = encoder as? JsonEncoder ?: throw SerializationException("API values require JSON")
json.encodeJsonElement(toJson(value))
}
/***************************************************************************
**
***************************************************************************/
override fun deserialize(decoder: Decoder): ListAny
{
val list = mutableListOf<Any?>()
decoder.decodeSerializableValue(serializer<SerializableListType>()).forEach {
list.add(it.value)
}
return ListAny(list)
/*******************************************************************************
** Decode objects uniformly, including objects whose keys happen to be type and value.
*******************************************************************************/
override fun deserialize(decoder: Decoder): Any? {
val json = decoder as? JsonDecoder ?: throw SerializationException("API values require JSON")
return fromJson(json.decodeJsonElement())
}
/*******************************************************************************
** Only serialization uses a locally supplied object's serializer; wire input never loads classes.
*******************************************************************************/
private fun toJson(value: Any?): JsonElement = when (value) {
null -> JsonNull
is JsonElement -> value
is String -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is Number -> JsonPrimitive(value)
is Map<*, *> -> JsonObject(value.entries.associate { entry ->
val name = entry.key as? String ?: throw SerializationException("JSON object keys must be strings")
name to toJson(entry.value)
})
is Iterable<*> -> JsonArray(value.map(::toJson))
else -> Json.encodeToJsonElement(serializer(value::class.starProjectedType), value)
}
/*******************************************************************************
** Preserve integer values and nulls while keeping arbitrary nested objects as ordinary maps.
*******************************************************************************/
private fun fromJson(value: JsonElement): Any? = when (value) {
JsonNull -> null
is JsonObject -> value.mapValues { fromJson(it.value) }
is JsonArray -> value.map(::fromJson)
is JsonPrimitive -> if (value.isString) value.content else
value.booleanOrNull ?: value.longOrNull ?: value.doubleOrNull
?: throw SerializationException("Invalid JSON primitive")
}
}
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
** Wrapper retained for callers that serialize arbitrary JSON object values.
*******************************************************************************/
@Serializable(with = MapStringAnySerializer::class)
data class MapStringAny(val entries: Map<String, Any?> = emptyMap())
/*******************************************************************************
** Delegate object framing to the standard map serializer.
*******************************************************************************/
object MapStringAnySerializer : KSerializer<MapStringAny> {
private val delegate = MapSerializer(String.serializer(), AnyValueSerializer)
override val descriptor = delegate.descriptor
/*******************************************************************************
** Delegate framing to the declared JSON serializer without introducing wire-controlled JVM class discovery.
*******************************************************************************/
override fun serialize(encoder: Encoder, value: MapStringAny) = delegate.serialize(encoder, value.entries)
/*******************************************************************************
** Reconstruct the declared wrapper through its JSON delegate instead of resolving classes from input.
*******************************************************************************/
override fun deserialize(decoder: Decoder) = MapStringAny(delegate.deserialize(decoder))
}
/*******************************************************************************
** Wrapper retained for callers that serialize arbitrary JSON lists.
*******************************************************************************/
@Serializable(with = ListAnySerializer::class)
data class ListAny(val entries: List<Any?> = emptyList())
/*******************************************************************************
** Delegate list framing to the standard list serializer.
*******************************************************************************/
object ListAnySerializer : KSerializer<ListAny> {
private val delegate = ListSerializer(AnyValueSerializer)
override val descriptor = delegate.descriptor
/*******************************************************************************
** Delegate framing to the declared JSON serializer without introducing wire-controlled JVM class discovery.
*******************************************************************************/
override fun serialize(encoder: Encoder, value: ListAny) = delegate.serialize(encoder, value.entries)
/*******************************************************************************
** Reconstruct the declared wrapper through its JSON delegate instead of resolving classes from input.
*******************************************************************************/
override fun deserialize(decoder: Decoder) = ListAny(delegate.deserialize(decoder))
}
@@ -1,36 +1,34 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model
import java.io.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@kotlinx.serialization.Serializable
data class QPossibleValue(
val id: Serializable,
val label: String
)
{
) {
}
@@ -1,31 +1,31 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
/***************************************************************************
**
***************************************************************************/
enum class AdornmentType
{
/*******************************************************************************
*******************************************************************************/
@kotlinx.serialization.Serializable(with = AdornmentTypeSerializer::class)
enum class AdornmentType {
UNKNOWN,
LINK,
CHIP,
SIZE,
@@ -34,3 +34,21 @@ enum class AdornmentType
REVEAL,
CODE_EDITOR,
}
/*******************************************************************************
** Keep unknown server enum values representable so additive metadata fails safely at the UI boundary.
*******************************************************************************/
object AdornmentTypeSerializer : kotlinx.serialization.KSerializer<AdornmentType> {
override val descriptor = kotlinx.serialization.descriptors.PrimitiveSerialDescriptor("AdornmentType", kotlinx.serialization.descriptors.PrimitiveKind.STRING)
/*******************************************************************************
** Map known wire names explicitly and preserve unsupported values through the enum's UNKNOWN member.
*******************************************************************************/
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): AdornmentType =
decoder.decodeString().let { value -> AdornmentType.entries.firstOrNull { it.name == value } ?: AdornmentType.UNKNOWN }
/*******************************************************************************
** Write the stable enum wire name rather than a JVM implementation detail.
*******************************************************************************/
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: AdornmentType) = encoder.encodeString(value.name)
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
@@ -24,14 +24,12 @@ package com.kingsrook.qqq.frontend.android.core.model.metadata
import com.kingsrook.qqq.frontend.android.core.model.MapOfStringAny
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class FieldAdornment(
val type: AdornmentType,
val values: MapOfStringAny? = emptyMap()
)
{
) {
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -24,9 +24,8 @@ package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QAppMetaData(
val name: String,
@@ -36,8 +35,7 @@ data class QAppMetaData(
val icon: QIcon? = null,
val widgets: List<String>? = emptyList(),
val sections: List<QAppSection>? = emptyList(),
// todo - not Any... val supplementalAppMetaData: Map<String, Any> = emptyMap()
)
{
val keywords: List<String>? = emptyList(),
) {
}
@@ -1,33 +1,51 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
/***************************************************************************
**
***************************************************************************/
enum class QAppNodeType
{
/*******************************************************************************
*******************************************************************************/
@kotlinx.serialization.Serializable(with = QAppNodeTypeSerializer::class)
enum class QAppNodeType {
UNKNOWN,
TABLE,
PROCESS,
REPORT,
APP,
}
/*******************************************************************************
** Keep unknown server enum values representable so additive metadata fails safely at the UI boundary.
*******************************************************************************/
object QAppNodeTypeSerializer : kotlinx.serialization.KSerializer<QAppNodeType> {
override val descriptor = kotlinx.serialization.descriptors.PrimitiveSerialDescriptor("QAppNodeType", kotlinx.serialization.descriptors.PrimitiveKind.STRING)
/*******************************************************************************
** Map known wire names explicitly and preserve unsupported values through the enum's UNKNOWN member.
*******************************************************************************/
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): QAppNodeType =
decoder.decodeString().let { value -> QAppNodeType.entries.firstOrNull { it.name == value } ?: QAppNodeType.UNKNOWN }
/*******************************************************************************
** Write the stable enum wire name rather than a JVM implementation detail.
*******************************************************************************/
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: QAppNodeType) = encoder.encodeString(value.name)
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -24,9 +24,8 @@ package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QAppSection(
val name: String,
@@ -35,16 +34,15 @@ data class QAppSection(
val tables: List<String>? = emptyList(),
val processes: List<String>? = emptyList(),
val reports: List<String>? = emptyList(),
)
{
val apps: List<String>? = emptyList(),
val role: String? = null,
val collapsedByDefault: Boolean? = null,
) {
/***************************************************************************
**
***************************************************************************/
fun hasContent(): Boolean
{
if(tables?.isEmpty() != false && processes?.isEmpty() != false && reports?.isEmpty() != false)
{
/*******************************************************************************
*******************************************************************************/
fun hasContent(): Boolean {
if (apps?.isEmpty() != false && tables?.isEmpty() != false && processes?.isEmpty() != false && reports?.isEmpty() != false) {
return (false)
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -24,9 +24,8 @@ package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QAppTreeNode(
val name: String,
@@ -34,7 +33,7 @@ data class QAppTreeNode(
val type: QAppNodeType,
val children: List<QAppTreeNode>? = emptyList(),
val icon: QIcon? = null,
)
{
val keywords: List<String>? = emptyList(),
) {
}
@@ -1,31 +1,30 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QBrandingMetaData(
val companyName: String? = null,
@@ -36,7 +35,6 @@ data class QBrandingMetaData(
val accentColor: String? = null,
val environmentBannerText: String? = null,
val environmentBannerColor: String? = null,
)
{
) {
}
@@ -1,31 +1,31 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
/***************************************************************************
**
***************************************************************************/
enum class QComponentType
{
/*******************************************************************************
*******************************************************************************/
@kotlinx.serialization.Serializable(with = QComponentTypeSerializer::class)
enum class QComponentType {
UNKNOWN,
HELP_TEXT,
BULK_EDIT_FORM,
BULK_LOAD_MAPPING,
@@ -39,3 +39,21 @@ enum class QComponentType
WIDGET,
HTML,
}
/*******************************************************************************
** Keep unknown server enum values representable so additive metadata fails safely at the UI boundary.
*******************************************************************************/
object QComponentTypeSerializer : kotlinx.serialization.KSerializer<QComponentType> {
override val descriptor = kotlinx.serialization.descriptors.PrimitiveSerialDescriptor("QComponentType", kotlinx.serialization.descriptors.PrimitiveKind.STRING)
/*******************************************************************************
** Map known wire names explicitly and preserve unsupported values through the enum's UNKNOWN member.
*******************************************************************************/
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): QComponentType =
decoder.decodeString().let { value -> QComponentType.entries.firstOrNull { it.name == value } ?: QComponentType.UNKNOWN }
/*******************************************************************************
** Write the stable enum wire name rather than a JVM implementation detail.
*******************************************************************************/
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: QComponentType) = encoder.encodeString(value.name)
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
@@ -25,9 +25,8 @@ import com.kingsrook.qqq.frontend.android.core.model.AnyValueSerializer
import com.kingsrook.qqq.frontend.android.core.model.QPossibleValue
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QFieldMetaData(
val name: String,
@@ -36,15 +35,34 @@ data class QFieldMetaData(
val defaultValue: Any? = null,
val type: QFieldType,
val isRequired: Boolean = false,
val isEditable: Boolean = false,
val isEditable: Boolean = true,
val isHeavy: Boolean = false,
val possibleValueSourceName: String? = null,
val inlinePossibleValueSource: List<QPossibleValue>? = null,
// Contingent fields only: DISABLE_UNTIL_SET (default when depending) or SHOW_ALL_UNTIL_SET.
val possibleValueSourceDependsOn: List<String>? = null,
val possibleValueSourceDependencyBehavior: String? = null,
val displayFormat: String? = null,
val adornments: List<FieldAdornment>? = null,
val helpContents: List<QHelpContent>? = null,
// todo - not Any... val behaviors: List<Any>?,
)
{
val isHidden: Boolean = false,
val validation: FieldValidation? = null,
) {
}
/*******************************************************************************
** Carry semantic field constraints so native validation follows the server contract.
*******************************************************************************/
@Serializable
data class FieldValidation(
val required: Boolean? = null,
val maxLength: Int? = null,
val tooLongBehavior: String? = null,
val minValue: String? = null,
val maxValue: String? = null,
val minInclusive: Boolean? = null,
val maxInclusive: Boolean? = null,
val pattern: String? = null,
val patternMessage: String? = null,
)
@@ -1,31 +1,31 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
/***************************************************************************
**
***************************************************************************/
enum class QFieldType
{
/*******************************************************************************
*******************************************************************************/
@kotlinx.serialization.Serializable(with = QFieldTypeSerializer::class)
enum class QFieldType {
UNKNOWN,
BOOLEAN,
STRING,
INTEGER,
@@ -39,3 +39,21 @@ enum class QFieldType
PASSWORD,
BLOB,
}
/*******************************************************************************
** Keep unknown server enum values representable so additive metadata fails safely at the UI boundary.
*******************************************************************************/
object QFieldTypeSerializer : kotlinx.serialization.KSerializer<QFieldType> {
override val descriptor = kotlinx.serialization.descriptors.PrimitiveSerialDescriptor("QFieldType", kotlinx.serialization.descriptors.PrimitiveKind.STRING)
/*******************************************************************************
** Map known wire names explicitly and preserve unsupported values through the enum's UNKNOWN member.
*******************************************************************************/
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): QFieldType =
decoder.decodeString().let { value -> QFieldType.entries.firstOrNull { it.name == value } ?: QFieldType.UNKNOWN }
/*******************************************************************************
** Write the stable enum wire name rather than a JVM implementation detail.
*******************************************************************************/
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: QFieldType) = encoder.encodeString(value.name)
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
@@ -25,9 +25,8 @@ import com.kingsrook.qqq.frontend.android.core.model.AnyValueSerializer
import com.kingsrook.qqq.frontend.android.core.model.MapOfStringAny
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QFrontendComponent(
val type: QComponentType,
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -24,9 +24,8 @@ package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QFrontendStepMetaData(
val name: String,
@@ -37,7 +36,6 @@ data class QFrontendStepMetaData(
val viewFields: List<QFieldMetaData>? = null,
val recordListFields: List<QFieldMetaData>? = null,
val helpContents: List<QHelpContent>? = null,
)
{
) {
}
@@ -1,37 +1,36 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QHelpContent(
val content: String,
val format: String,
val roles: Set<String> = emptySet()
)
{
val roles: Set<String> = emptySet(),
val contentAsText: String? = null
) {
}
@@ -1,37 +1,36 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QIcon(
val name: String? = null,
val path: String? = null,
val color: String? = null,
)
{
val semanticName: String? = null,
) {
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -24,20 +24,17 @@ package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QInstance(
val tables: Map<String, QTableMetaData>? = emptyMap(),
val processes: Map<String, QProcessMetaData>? = emptyMap(),
// todo val reports: Map<String, Any>?, // todo
val widgets: Map<String, QWidgetMetaData>? = emptyMap(),
val apps: Map<String, QAppMetaData>? = emptyMap(),
val appTree: List<QAppTreeNode> = emptyList(),
val branding: QBrandingMetaData? = QBrandingMetaData(),
val environmentValues: Map<String, String?> = emptyMap(),
val helpContent: Map<String, List<QHelpContent>>? = emptyMap(),
)
{
}
) {
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -24,9 +24,8 @@ package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QProcessMetaData(
val name: String,
@@ -37,6 +36,5 @@ data class QProcessMetaData(
val frontendSteps: List<QFrontendStepMetaData>? = emptyList(),
val hasPermission: Boolean = false,
val stepFlow: String = "LINEAR",
)
{
}
) {
}
@@ -1,34 +1,32 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2024-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QProcessMetaDataWrapped(
val process: QProcessMetaData
)
{
}
) {
}
@@ -1,22 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2004-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
@@ -24,16 +24,13 @@ package com.kingsrook.qqq.frontend.android.core.model.metadata
import kotlinx.serialization.Serializable
/***************************************************************************
**
***************************************************************************/
/*******************************************************************************
*******************************************************************************/
@Serializable
data class QTableMetaData(
val name: String,
val label: String,
val icon: QIcon? = null,
val isHidden: Boolean = false,
// todo! all the other fields
)
{
}
) {
}
Loaded 100 of 2517 files, more files were not shown because too many files have changed in this diff. Show more