feat(android): add native display and embedded announcements

This commit is contained in:
2026-08-12 19:21:47 +08:00
parent e4e3e6a735
commit 50ac5bddc4
69 changed files with 4574 additions and 800 deletions

View File

@@ -0,0 +1,85 @@
# Task: Implement native Android display screens
## Identity
- Task ID: 20260811-native-display-f2456ea9
- Mode: Feature
- Branch: main
- Worktree: D:\Datas\PythonProjects\XQKqueue
- Base commit: e4e3e6a735c1a1a485d87a979b80c1ebe5803b6f
- Owner: codex
- Status: Blocked at documentation completion gate; Android 1.5.2 range-display fix and APK are complete and ready for target-device acceptance
## Scope
- Replace the Android WebView presentation shell with native Android overview and project-detail screens.
- Call the existing public overview and project snapshot APIs directly from the APK.
- Preserve project navigation, polling, new-call deduplication, three-repeat announcement playback, immersive landscape display, error recovery, and deliverable APK documentation.
- Replace Android system TTS with APK-embedded Mandarin PCM audio fragments composed from the server-provided ticket numbers.
- Keep the existing server and browser UI behavior unchanged unless an API-contract defect is proven.
## Intent And Constraints
- The user explicitly chose native Android because the target display device remains unreliable with WebView rendering and system TTS.
- The overview screen must never announce calls; only a concrete `/display/{identifier}` project screen may announce.
- The target panel is 1920x1080 landscape, but layout must use available dimensions/density rather than hard-coded physical pixels.
- Existing uncommitted landscape/WebView work and deliverables were explicitly adopted as the starting worktree state.
- Facts must be verified against current server and Android source; unknown API fields must not be invented.
- The server contract and database must remain unchanged; the APK owns number-to-audio composition.
- Embedded voice assets must have a documented, distributable source and a uniform decoder-independent PCM WAV format.
## Outcome
- Replaced the Android WebView shell with native platform Views for the project overview and project detail screens.
- Added strict public-display DTO parsing, a typed HTTP client, and one generation-safe fixed-delay polling loop per active screen.
- The overview polls every 5 seconds, supports an adaptive 1-4 column project-card grid and DPAD/touch navigation, and has no announcement coordinator.
- The project screen polls every 3 seconds; the same successful snapshot updates the UI and drives announcement deduplication.
- Preserved first-snapshot silence and replaced system TTS with 13 APK-embedded Mandarin PCM WAV fragments. The app composes strict five-digit single/range calls locally, streams each new batch three times through `AudioTrack`, and never includes the project name.
- Rapid new batches remain FIFO ordered, with 350 ms gaps between repeats and batches. Pause, navigation away, audio-focus loss, or shutdown stops the active/pending queue; overview construction has no announcement path.
- Streaming uses bounded 16 KiB writes and transient media audio focus instead of a long static native buffer, reducing Android TV implementation and memory risk.
- Added stable loading, empty, 404, fatal-error, cached-offline, and delayed-update states; forecast pages rotate every 6 seconds independently of the 3-second refresh.
- Reused the existing scenic logo and water artwork in native resources and retained density-adaptive fixed-landscape behavior.
- Removed the obsolete WebView-only announcement poller. After target-device feedback proved the first eSpeak voice assets did not produce Chinese speech, restored the project's original browser WAV fragments from Git history and remapped them to the Android composer. Produced debug APK version 1.5.1 (versionCode 8).
- Target-TV evidence then showed a two-ticket batch rendering only `00010 至`. The live snapshot was verified to contain both `00010` and `00011`, proving the API and formatter were complete. Replaced the current-number platform auto-size dependency with an API-23-compatible `SingleLineFitTextView` that measures the real content width and height, chooses the largest fitting size, never ellipsizes, and can shrink below the nominal 30sp floor rather than hide the last ticket. Produced debug APK version 1.5.2 (versionCode 9).
## Verification
- `android\gradlew.bat clean testDebugUnitTest lintDebug assembleDebug`: PASS after the final 1.5.2 range-display fix; 56 JVM tests in 12 suites, 0 failures/errors/skipped; lint 0 errors / 24 warnings.
- Web Vitest: PASS, 18 files / 66 tests.
- Web TypeScript checks and Vite production build: PASS.
- `go test ./...` in `server`: PASS.
- `git diff --check`: PASS (only repository line-ending notices).
- APK source and deliverable SHA-256 match: `5CB13105178EA5630EA124D0BE98A189E1B7F0557B2672C8CFAD3B6002FCBBEB`.
- `aapt`: package `cn.nianxx.queue.display`, versionCode 8, versionName 1.5.1, minSdk 23, targetSdk 36, landscape activity, no TTS service query, and all 13 voice WAV resources packaged.
- `apksigner verify`: PASS with Android Debug certificate (v1/v2 signatures).
- Actual voice resources are locked by JVM tests for exact names, strict WAV parsing, PCM16 mono 16 kHz format, frame alignment, and common sample rate. Git-history source, mapping, hashes, failed 1.5.0 diagnosis, and 1.5.1 ASR evidence are recorded in `50-evidence/topics/20260811-native-display-f2456ea9__embedded-audio-assets.md`.
- The 1.5.0 eSpeak `cmn` audio failed calibrated SAPI and Vosk recognition and was replaced. The restored legacy WAVs recover the ticket phonemes and sentence anchors with Vosk average confidence around 0.84-0.89. No ADB device or configured emulator was available, so target-TV listening remains required.
- Final audio read-only Sol review: PASS; no P0/P1 findings after streaming, audio-focus cancellation, and stop/release serialization fixes.
- Range-display regression: an API-25 Robolectric view test reproduces the real `00010 至 00011` batch through the complete native `ProjectDisplayView` and locks full text/single-line/no-ellipsis behavior. A narrow ASCII view test asserts the final measured width and font height fit the real content bounds. A deterministic test calls the production two-stage sizing decision and locks largest-fitting selection, below-30sp fallback, and the 1sp terminal boundary. The pre-fix API-25 case kept the initial 74sp because the old helper only enabled platform auto-size on API 26+.
- Live API verification on 2026-08-12: `/api/display/YYHHC/snapshot` returned `current_batch.tickets=[00010,00011]` and `latest_ticket_number=00011`; this excluded the server contract and parser as the truncation source.
- APK source and deliverable SHA-256 match after the final 1.5.2 build: `0D56C10B8CE706893F3EA8E59B21329847517F56F7347ABEA127AB30C6C71D77`; `aapt` reports versionCode 9 / versionName 1.5.2 and `apksigner` verifies v1/v2 signatures.
- Final range-display read-only Sol review: code and APK PASS with no P0/P1 implementation findings. Overall project-task status remains FAIL solely because the mandatory project-document drift gate is blocked by the pre-existing initializer `FOREIGN_DOC` state described below.
- Task Documentation Gate: `check_doc_drift.py --task-id 20260811-native-display-f2456ea9` remains BLOCKED because the document initializer created canonical templates that the feature-task boundary classifies as `FOREIGN_DOC`; product source, task-owned record, and task-prefixed evidence are otherwise present. `task_context complete` was not run because the drift prerequisite did not pass.
## Follow-ups
- Install the 1.5.2 debug APK on the target 1920x1080 television and confirm `00010 至 00011` plus a wider representative range show both endpoints; also smoke-test overview navigation, project refresh, DPAD focus, Home/resume silence, media volume/focus routing, and every digit in representative calls.
- Confirm and archive the ownership/redistribution basis for the legacy project WAVs; Git history contains the files and usage but no original generator, speaker, or separate license record.
- Build the production release with the operator's long-lived signing certificate rather than the debug certificate.
- Revalidate orientation behavior before moving to targetSdk 37 because the Android 16 compatibility property is temporary.
- Documentation gate: obtain human authorization to re-register this task in Integration mode so the initializer-created canonical `.project-docs` templates can be owned without committing, hiding, or deleting them.
## Promotion Candidates
- Target: architecture/system overview and data-flow documentation.
Proposal: record that the dedicated Android display client is native Views rather than WebView; exactly one active screen poller owns network refresh; a project snapshot is the single source for both rendering and embedded-audio announcements; the overview screen cannot announce by construction.
Evidence: native Activity/data/view/audio implementation, 53 JVM tests, lint/build results, and API/audio evidence under `50-evidence/topics/`.
Future impact: Android display changes should extend the native DTO/view boundary and must not introduce a second snapshot request or announcement path on overview.
Conflicts: none known; the canonical files were initialized as placeholders and were intentionally not promoted during this feature task.
Confirmation: the user explicitly approved replacing the current Android presentation with native pages, taking over the existing worktree changes, and replacing TTS with embedded audio.
- Target: the `maintain-project-docs` initialization workflow.
Proposal: provide an explicit first-install ownership mode so initializer-created canonical templates do not appear as foreign feature-task drift.
Evidence: `check_doc_drift.py` classified every non-overwritten initializer template as `FOREIGN_DOC`; see `60-reflection/cases/20260811-native-display-f2456ea9__initialization-drift-deadlock.md`.
Future impact: avoids a completion deadlock when the document system is first introduced during a repository task.
Conflicts: none with product behavior; this concerns task-document workflow only.
Confirmation: required before changing task mode or shared skill behavior.

View File

@@ -0,0 +1,52 @@
# Task: Integrate native Android display
## Identity
- Task ID: 20260812-integrate-native-display-1a52b7c9
- Mode: Integration
- Branch: main
- Worktree: D:\Datas\PythonProjects\XQKqueue
- Base commit: e4e3e6a735c1a1a485d87a979b80c1ebe5803b6f
- Owner: codex
- Status: Ready for bootstrap commit
## Scope
- Adopt and integrate the completed native Android display work already present in the main worktree.
- Include the native overview and project screens, public display API client, detail-only announcements, embedded Mandarin WAV playback, 1920 x 1080 landscape behavior, range-number fitting, tests, and the updated debug APK.
- Preserve the existing responsive Web display adjustment that was developed and verified in the same adopted worktree.
- Carry the source task's task-owned evidence into version control without rewriting its historical record.
## Intent And Constraints
- The user explicitly authorized taking over the existing changes and later requested that they be merged and pushed to `main`.
- `origin/main` and local `main` both pointed to `e4e3e6a735c1a1a485d87a979b80c1ebe5803b6f` when integration began.
- The overview screen must never announce. Only `/display/{project-code}` may poll announcement state and play audio.
- Audio must use the APK's embedded Mandarin PCM assets and repeat each announcement three times; it must not depend on Android TTS.
- A batch range such as `00010 至 00011` must remain complete, single-line, and unellipsized on the supported Android UI.
- Source task records are retained as historical evidence. Canonical project-memory promotion will be completed from a clean follow-up Integration task whose base includes this bootstrap commit, because the source task's previously uncommitted task-owned documents are foreign to this Integration task's base.
## Outcome
- Prepared one bootstrap integration unit containing the adopted product changes, deliverable APK, source task record, evidence, and reflection.
- Canonical project-memory promotion and remote push remain pending.
## Verification
- Android: `clean testDebugUnitTest lintDebug assembleDebug` passed with 56 tests, zero failures, and zero lint errors.
- Web: 66 Vitest tests and production build/typecheck passed during the source task.
- Server: `go test ./...` passed during the source task.
- APK: versionCode 9 / versionName 1.5.2, minSdk 23, targetSdk 36, v1/v2 signatures valid.
- Source and deliverable APK SHA-256 matched `0D56C10B8CE706893F3EA8E59B21329847517F56F7347ABEA127AB30C6C71D77`.
- Independent range-display review found no P0/P1 code or APK blocker; the remaining blocker was only the project-document ownership transition addressed by this bootstrap step.
## Follow-ups
- Start a follow-up Integration task at the bootstrap commit, promote durable facts into canonical project memory, run the final documentation gate, obtain a fresh read-only review, and push `main`.
## Promotion Candidates
- Native Android Views are the supported large-screen application implementation; the Web display remains a separate browser surface.
- The snapshot response is the single source for project-screen UI and announcement state.
- Announcement lifecycle is structurally detail-only, with cancellation when leaving the project screen or pausing the Activity.
- Embedded PCM assets and `AudioTrack` streaming replace device TTS for appliance compatibility.

View File

@@ -0,0 +1,61 @@
# Embedded announcement audio evidence
## Current accepted source
Android version 1.5.1 reuses the queue announcement WAV fragments that already existed in this repository's browser implementation. They were first added in commit `6d911271e6bf5b0120edc04c91506077239e96e5`, moved byte-for-byte into `web/src/assets/queue-voice/` in commit `4405bf96f7ad8029fbf62741d92ddaab7b99c096`, and later removed from the browser bundle in commit `5ad8bfcd0377e6dfb8c703d0834d248633a5b939` when that implementation changed to `speechSynthesis`.
The user confirmed that the project already had these WAV assets; that evidence redirected the fix to reuse the project's own prior assets instead of generating another voice. Repository history does not record the original generator, speaker, recording author, or a separate license/NOTICE. Therefore this record treats them as project-owned legacy assets in scope for this repair; it does not claim that they are open source or independently cleared for third-party redistribution.
## Android mapping
| Android resource | Historical source / operation | Spoken content |
| --- | --- | --- |
| `voice_prompt.wav` | `please.wav` | 请 |
| `voice_digit_0.wav` ... `voice_digit_9.wav` | corresponding `digit-0.wav` ... `digit-9.wav` | 零 ... 九 |
| `voice_range.wav` | PCM concatenation of `number.wav` then `through.wav` | 号至 |
| `voice_suffix.wav` | PCM concatenation of `number.wav` then `entrance.wav` | 号,前往入口 |
The two concatenated resources contain one RIFF header and the source PCM samples in the listed order. Android creates its repeat and batch gaps, so the historical `gap.wav` is not packaged.
## Format and integrity
All 13 Android resources parse as RIFF PCM format tag 1, 16-bit, mono, 16 kHz, with aligned sample data. `EmbeddedVoiceAssetsTest` locks the exact resource set and validates every committed file through the production WAV parser.
| Resource | SHA-256 |
| --- | --- |
| `voice_digit_0.wav` | `DA7E001528495F75473F99A6BBC275B9C4B7E5FC0888FA9495F86E16231DDC61` |
| `voice_digit_1.wav` | `2248F327AC9386ECEADB98A4A24F857C218596406EE519F268D67FEEF785971D` |
| `voice_digit_2.wav` | `DFCD91AA933F78E5CC4BB18B84A9F00D5365C7992B15EB7948960BFBE4505EE6` |
| `voice_digit_3.wav` | `98FD18056AF18D3B951E6777D5CA2EE7971005F9FBFD5054E8DADDDA4913778F` |
| `voice_digit_4.wav` | `073B17DCC81F416799644A34A75723CE1B75A8D90D06679D4F0E6F96DB9CBBFE` |
| `voice_digit_5.wav` | `80DFA4AD87E752CE23F49B123D91E9E06F691267CBD3ECA94FF12213C73739D7` |
| `voice_digit_6.wav` | `131A747A9D0C8825F8C69907E19E9FB83935D2EB6FEB1137F6EF0311F96F197E` |
| `voice_digit_7.wav` | `7DAF943934080733D7F6704CE58FCADC736965A578DC0A091CBFDCCB8E4B9BE4` |
| `voice_digit_8.wav` | `1406CF912AB2756A6A6600749767FFA0181832DADB28FA0CD81FBB0F1BE47A48` |
| `voice_digit_9.wav` | `BE72EC45438D4D2835E11B91380715CE36650CC465D5EE5465B540B1B8686F8F` |
| `voice_prompt.wav` | `C066CAF5107994D4B7F7E96E6818AF54E8BB785914FD5881FA223C18C3CF9BDE` |
| `voice_range.wav` | `9018BF8EF743F6A6FC2323BDE7313CCFE12D5E14A19207E87C2EE3B3A19D9C7B` |
| `voice_suffix.wav` | `010CF52AE95B0F541A105DB43741832A75FB670A3D02A72A032D94D9AC95182E` |
## Semantic diagnosis and acceptance
The 1.5.0 eSpeak NG `cmn` assets were rejected after the target user reported that they did not sound Chinese. Reproduction showed correct byte order and repetition, but two Chinese recognizers returned unrelated text at approximately 0.001-0.002 confidence. Direct unfragmented synthesis from the same engine also failed, confirming the source voice mapping rather than Android composition as the cause.
The restored legacy assets were evaluated in the actual Android token order:
- `00017`, one pass: `请 + 0 + 0 + 0 + 1 + 7 + 号 + 前往入口`.
- `00017-00020`, one pass: `请 + 0 + 0 + 0 + 1 + 7 + 号至 + 0 + 0 + 0 + 2 + 0 + 号 + 前往入口`.
- Android repeats the pass three times with two 350 ms zero-PCM gaps.
Vosk small-cn 0.22 recovered the complete target phoneme sequence and sentence structure from final Android-composed samples, with average word confidence approximately 0.84-0.89. Its text used homophones/segmentation such as `一期` for `一七` and `好` for `号`, while all three repeats remained stable. Windows zh-CN SAPI also recovered the sentence anchors but was less accurate on isolated digit fragments. A digit calibration sequence recovered 零 through 九 in order. These are calibrated automated red-flag checks, not a substitute for human acceptance.
The final 1.5.1 single-number preview produced from the committed Android resources is 11.516125 seconds and has SHA-256 `9FFDA2837EC40DAF4AC626FF093DDC64223A6F9E352FA3F8E198A48C97C285D3`.
## Verification and remaining risk
- `gradlew.bat clean testDebugUnitTest lintDebug assembleDebug`: PASS; 53 JVM tests, no failures/errors/skips; lint has zero errors and 23 warnings.
- APK version: versionCode 8, versionName 1.5.1.
- APK contains exactly the 13 required `res/raw/voice_*.wav` resources.
- APK SHA-256: `5CB13105178EA5630EA124D0BE98A189E1B7F0557B2672C8CFAD3B6002FCBBEB`.
Remaining acceptance: listen to representative single and range announcements on the target television and confirm every digit. The historical clips have uneven source loudness; they were kept byte-exact rather than normalized because processing would invalidate their prior use and requires renewed listening/ASR review.

View File

@@ -0,0 +1,35 @@
# Evidence: Public display API contract
- Date: 2026-08-11
- Status: Verified
- Scope: Native Android overview and project-detail implementation
- Confidence: High
- Sources: `server/internal/httpapi/public.go`, related Go tests, and live HTTPS responses from `queue.nianxx.cn`
- Last verified: 2026-08-11
- Stale trigger: public display routes or DTOs change, or production is deployed from a server revision that differs from this repository
## Confirmed endpoints
- `GET /api/display/overview` returns `{ projects: [...], server_time }` without authentication.
- `GET /api/display/{identifier}/snapshot` accepts a normalized project code or a 40-128 character display token.
- Invalid or unknown identifiers return `DISPLAY_NOT_FOUND` with HTTP 404.
## Confirmed overview project fields
- `id`, `code`, `name`, `status`
- `waiting_count`, `waiting_ticket_count`, `waiting_people_count`
- `issued_ticket_count`, nullable `latest_ticket_number`, `experienced_people`
- nullable `current_batch`, `estimated_wait`, `last_updated_at`
## Confirmed snapshot fields
- Project: `project_name`, `status`, nested `project`
- Queue: `revision`, the three waiting counts, issued/latest/experienced metrics
- Calls: nullable `current_batch`, `recent_batches`
- Timing: `estimated_wait`, `server_time`, `last_updated_at`
Each call batch exposes `batch_number`, `sequence`, `status`, `call_mode`, requested/ticket/people counts, `called_at`, and tickets. Each ticket exposes public display number fields, status, and party size; no personal visitor fields are present.
## Implementation consequence
The native detail screen can render and decide announcements from one snapshot request every three seconds. A second announcement-only request is unnecessary. The first successful snapshot after entering or resuming a project must establish a baseline and must not replay the current batch.

View File

@@ -0,0 +1,48 @@
# Native range-display truncation evidence
## Reported symptom
On the target 1920x1080 television, a batch that called tickets `00010` and `00011` rendered the main current-call field as only `00010 至`; the latest-ticket metric at the right still showed `00011`.
## Boundary verification
The production public snapshot was read on 2026-08-12 from `GET https://queue.nianxx.cn/api/display/YYHHC/snapshot`. It contained:
- `latest_ticket_number: "00011"`
- `current_batch.ticket_count: 2`
- `current_batch.tickets[0].display_number: "00010"`
- `current_batch.tickets[1].display_number: "00011"`
`DisplayFormatting.currentBatchNumbers` therefore produces the complete string `00010 至 00011`. This rules out the server response, JSON parser, and range formatter as the cause.
## Root cause
The original native current-number `TextView` started at 74sp, allowed one line, and called `NativeDisplayUi.autoSize`. That helper only activated Android platform auto-size on API 26 and later, although the APK supports API 23. On API 23-25, long range text stayed at 74sp and could be silently clipped by the half-width current-call panel. Platform behavior and display density also made the guarantee device-dependent on newer releases.
An API-25 Robolectric regression using a full `ProjectDisplayView`, 1920x1080 exact layout, and the real two-ticket snapshot was run before the fix. Its red signal confirmed the legacy view retained the initial 74sp rather than applying a fitting policy.
## Fix and invariant
`SingleLineFitTextView` now owns current-call fitting on API 23 and later. On text, size, or padding changes it:
- computes actual content width and height after compound padding;
- measures the real text and font metrics with a copy of the active `TextPaint`;
- binary-searches for the largest whole-sp size that fits both dimensions;
- enforces a single line and never ellipsizes;
- falls below the normal 30sp readability floor only when necessary to preserve the complete ticket range.
Short single numbers still receive the largest fitting size, up to 74sp.
## Verification
- `gradlew.bat clean testDebugUnitTest lintDebug assembleDebug`: PASS.
- JVM tests: 56 across 12 suites, 0 failures, 0 errors, 0 skipped.
- Lint: 0 errors, 24 warnings.
- `ProjectDisplayViewTest` locks the full `00010 至 00011` string, one-line behavior, and no ellipsis on API 25.
- A narrow ASCII control asserts the fitted `TextPaint` width and font height are within actual content bounds; deterministic sizing tests call the production two-stage decision and lock largest-fitting selection, full-size selection, below-floor fallback, and the 1sp terminal boundary.
- APK: versionCode 9, versionName 1.5.2; Android Debug v1/v2 signatures verified.
- Build and deliverable SHA-256: `0D56C10B8CE706893F3EA8E59B21329847517F56F7347ABEA127AB30C6C71D77`.
## Remaining acceptance
Robolectric's host font metrics are not the target television's font metrics. The production code uses the device's real `TextPaint`, but final acceptance still requires installing 1.5.2 on the target television and visually confirming both endpoints for `00010 至 00011` and at least one wider representative range.

View File

@@ -0,0 +1,27 @@
# Reflection: Project-docs initialization and feature drift
- Task: `20260811-native-display-f2456ea9`
- Date: 2026-08-11
- Status: Confirmed workflow gap in this repository state
## What happened
The repository had no `.project-docs/` tree. The required initializer created the canonical templates before the feature task was claimed. The feature task then correctly limited its intentional documentation writes to its own task record and prefixed evidence/reflection records.
At completion, `check_doc_drift.py` compared the feature task's recorded base commit with the worktree. Because every initializer-created canonical template was still untracked, the script classified all of them as protected `FOREIGN_DOC` changes. A feature task cannot accept those paths, while removing them would leave a partial document system and violate the initialization contract.
## Why it matters
On a repository that first adopts this skill during an already dirty feature workflow, the documented sequence “initialize, then claim a feature task” cannot by itself produce a passing Task Documentation Gate unless the initialization tree is first established in a separate canonical baseline.
## Better workflow
Initialize `.project-docs/` in a serialized integration task and establish that tree as a reviewed repository baseline before starting feature work. If the initializer must run during feature intake, the tooling should provide an explicit, human-confirmed initialization ownership path rather than requiring agents to hide, delete, or manually reclassify protected files.
## Promotion candidate
- Target: `maintain-project-docs` initialization workflow and/or drift script.
- Proposal: add a first-install mode that atomically records initializer-owned canonical files, or explicitly require and guide a serialized initialization integration before feature task creation.
- Evidence: this task's drift output lists every initializer template as `FOREIGN_DOC` even though no feature agent edited those templates after initialization.
- Future impact: prevents completion deadlocks on repositories adopting the document system for the first time.
- Human confirmation: required before changing the shared skill or committing a new canonical documentation baseline.