# PHASE X4.11-I2 — PHASE 0: KITCHEN AGENT REFERENCE AUDIT

Date: 2026-10-07
Mode: READ-ONLY
Kitchen project: `C:\Users\M S I\Documents\PROJECT\POS SATE\android-print-agent` (FROZEN — not modified)

## 1. Project structure

**Gradle structure:**
- Root files: `build.gradle`, `settings.gradle`, `local.properties`, `README.md`
- No `gradlew` wrapper in repo — toolchain is external `C:\Users\M S I\x4.25-toolchain\gradle-7.6.4`
- Known-good command (proven X4.25): `GRADLE_USER_HOME=.../gradle-home gradle-7.6.4/bin/gradle --no-daemon assembleDebug` → `build/outputs/apk/debug/pos-print-agent-debug.apk` (119,841 bytes)
- `settings.gradle`: `pluginManagement { google(), mavenCentral(), gradlePluginPortal() }` + `dependencyResolutionManagement { google(), mavenCentral() }`, `rootProject.name = "pos-print-agent"`
- `build.gradle`: `plugins { id 'com.android.application' version '7.4.2' }`, `android { namespace 'agent.android', compileSdk 33, defaultConfig { applicationId "id.sategajah.printagent", minSdk 29 (Android 10+), targetSdk 33, versionCode 1, versionName "0.1.0-mvp" }, compileOptions Java 11, sourceSets { main { manifest 'src/main/AndroidManifest.xml', java.srcDirs = ['src'] } }, dependencies {} }` — zero third-party deps
- `local.properties`: `sdk.dir=C:\Android\sdk` (platform android-33, build-tools 33.0.1)

**Android SDK / build config:**
- CompileSdk 33, minSdk 29, targetSdk 33, AGP 7.4.2, Gradle 7.6.4, JDK 11, SDK `C:\Android\sdk`

**Package / application ID:**
- Application ID: `id.sategajah.printagent`
- Label: `POS Print Agent` (`AndroidManifest.xml`)
- Java package: `agent.android` (service/activity/receiver) + `agent.core` (pure JVM)

**Java/Kotlin:**
- 100% Java (no Kotlin) — consistent with repo's zero-dependency philosophy and JDK-11 testability

**Source / test layout:**
```
android-print-agent/
  src/
    agent/core/        Json.java, Cp437.java, AgentPolicy.java, DeviceApiProtocol.java, EscPosRenderer.java, PrintPipeline.java
    agent/android/     SecureTokenStore.java, FileLog.java, BootReceiver.java, MainActivity.java, PrintAgentService.java, BluetoothPrinter.java
    main/AndroidManifest.xml
  test/                RunTests.java, EscPosRendererTest.java, JsonTest.java, DeviceApiProtocolTest.java, AgentPolicyTest.java, PrintPipelineTest.java (48 tests, `javac -d out src/agent/core/*.java test/*.java && java -cp out agent.test.RunTests`)
  test-android/        BluetoothPrinterTest.java, RunBluetoothTests.java (9 tests, stale-idle decision layer, needs android.jar)
  build/outputs/apk/debug/pos-print-agent-debug.apk
  build.gradle / settings.gradle / local.properties / README.md
```

**APK output:** `build/outputs/apk/debug/pos-print-agent-debug.apk` (debug, BUILD SUCCESSFUL)

**Build command:** see above (proven).

## 2. Runtime architecture

```
MainActivity (setup only)
  │  Server URL, Device Token (SecureTokenStore), paired BT printer (BluetoothPrinter.pairedDevices), TEST CONNECTION (/heartbeat), TEST PRINT (local ESC/POS, no print_jobs), START/STOP service
  ↓
PrintAgentService (Foreground Service, START_STICKY, notification ongoing, FOREGROUND_SERVICE_CONNECTED_DEVICE)
  │  HttpTransport (HTTPS-only, 10s timeout) — polling loop
  ↓
PrintPipeline (one claim → print → ACK cycle)
  │  DeviceApiProtocol (claim/result parsing), AgentPolicy (idle 3s, backoff ×2 cap 30s, ACK 3× 1s, transport threshold 6×http=0, lease 90s), FileLog
  ↓
EscPosRenderer (plain text → ESC/POS bytes: init ESC @, wrap per CPL, center header until separator, cut GS V 66 0 opt, CP437 + UTF-8 passthrough)
  │  Cp437 mapping (· → 0xFA, unmappable → '?')
  ↓
BluetoothPrinter (SPP RFCOMM UUID 00001101-0000-1000-8000-00805F9B34FB, paired-only, secure→insecure fallback, settle delays 300ms/500ms, retry MAX 2, stale-idle 60s precheck)
  ↓
Bluetooth thermal printer (58mm, Font A 32 CPL)
```

**Responsibilities:**
- MainActivity: configuration UI only (no POS logic), permission handling (BLUETOOTH_CONNECT runtime, Android 12+), never touches print_jobs.
- SecureTokenStore: Android Keystore AES-GCM, ciphertext in prefs (`pos_agent_secure`), never plaintext/logs/source.
- FileLog: token-safe daily file `agent-YYYYMMDD.log`, safeError sanitized.
- BootReceiver: BOOT_COMPLETED → restart service if config complete (server URL + printer MAC + token).
- PrintAgentService: loop, notification state RUNNING/SERVER_UNREACHABLE/BLUETOOTH_DISCONNECTED/ERROR/STOPPED, HttpTransport (Bearer token).
- PrintPipeline: one runOnce() per poll; lease-aware; never re-prints locally on ACK timeout; never re-claims on claim timeout.
- EscPosRenderer: pure function, JVM-testable.
- BluetoothPrinter: transport-level success (write+flush), not paper confirmation.

## 3. Server communication

**Endpoints (from DeviceApiProtocol.java + DeviceApiController):**
- `POST /api/v1/device/print-jobs/claim` — Bearer header only, 64KB guard, 1s rate limit on live lease. Responses: 204 = no job (also 200-empty/“null”/[]/{} per bridge contract), 200 = `{job_id, claim_token, destination, payload, attempt_count, lease_expires_at, server_time}`, 401/403/429/5xx.
- `POST /api/v1/device/print-jobs/{jobId}/result` — body `{"claim_token":"…","result":"SUCCESS"|"FAILURE","error_message":"…"≤255}`, Responses: 200 `{status:"PRINTED",idempotent:bool}`, 409 `STALE_CLAIM`/`INVALID_STATE`, 422, 401.
- `POST /api/v1/device/heartbeat` — read-only liveness (used by TEST CONNECTION, no claim/job).

**Auth:** Bearer token (raw 64-hex, SHA-256 hash server-side), AKTIF status required for claim, `last_seen_at` updated. Insecure token in config is dev-only warning.

**Claim flow:** server derives branch + destination from device registration (client branch_id ignored), FIFO `FOR UPDATE`, random claim_token per generation, lease 90s fixed, attempt_count per generation.

**Claim token / lease:** token is authoritative identity of claim generation; stale result → STALE_CLAIM 409 (no retry print); lease expiry → job re-claimable (retained claim_token/claimant).

**ACK/result:** SUCCESS → PRINTED (+printed_at, idempotent on repeat ACK same token), FAILURE → FAILED (requires error_message, re-claimable while attempts<3), repeated ACK for PRINTED = idempotent 200.

**Retry:** server-bounded 3 attempts; client ACK retry 3× 1s on same claim_token; consecutive http=0 threshold 6 → controlled exit (Windows) / SERVER_UNREACHABLE (Android).

**HTTP errors:** 401 → STOP (operator), 429 → backoff, 5xx → backoff ×2 cap 30s, reset to 3s on any real HTTP response.

## 4. Bluetooth architecture

- **Bluetooth Classic SPP/RFCOMM**, UUID `00001101-0000-1000-8000-00805F9B34FB` (standard SPP).
- **Paired-only** — `getBondedDevices()`, no background scanning; Android 12+ BLUETOOTH_CONNECT guarded (empty set on SecurityException, never crash).
- **Secure RFCOMM primary** (`createRfcommSocketToServiceRecord`), **insecure fallback** (`createInsecureRfcommSocketToServiceRecord`) if secure fails; settle 300ms after connect, 500ms after write.
- **Connection handling:** one BluetoothPrinter instance per service, synchronized, single socket+stream, `connectedDevice` retained for reconnect, `connectionMode` SECURE/INSECURE.
- **Stale handling (X4.x-FIX):** idle >60s (`STALE_CONNECTION_MAX_IDLE_MS=60_000`, baseline `lastSuccessfulWriteMs`) → suspect stale → close → reconnect once BEFORE next write (not before every print, no periodic loop, no ping bytes — ping would print garbage). Fresh connection without baseline never suspected.
- **Write retry:** `MAX_WRITE_ATTEMPTS=2` — attempt1 write+flush; on IOException → close → reconnect (same device) → attempt2. Catch `IOException|SecurityException`. Success = write+flush completed (transport-level, not paper).
- **Reconnect:** deterministic, same paired device, secure→insecure again.
- **Duplicate protection:** at-least-once (same as Windows bridge); retry only once, documented; no local queue.

## 5. Rendering

- **ESC/POS renderer:** `EscPosRenderer.render(text, Options)` — pure, JVM-testable. Options: `charsPerLine` (58mm Font A ≈32), `centerHeader` (center until separator `----`), `cut` (GS V 66 0), `utf8Passthrough`.
- **Character width:** CPL config (32 default, 48 for 80mm), word-boundary wrap, hard-break fallback.
- **Cut:** optional partial cut, enabled by default.
- **UTF-8:** default CP437 mapping (Rp, · 0xFA, Indonesian product names; unmappable → '?'), UTF-8 passthrough mode when printer supports it.
- **Payload format:** server sends plain text UTF-8 lines `\n`-joined (kitchen batch: order_number/CABANG/MEJA/BATCH/WAKTU/PELANGGAN/separator/items/notes; voucher similar). Android renders verbatim — no server ESC/POS.
- **Server vs Android:** server = text, Android = bytes (ESC @, alignment `ESC a n`, lines, feed, cut). No QR/barcode/logo/drawer-kick in MVP; no bold.

## 6. Security

- **Token storage:** `SecureTokenStore` — Android Keystore AES-GCM (`KEY_ALIAS pos_agent_device_token`), `Cipher AES/GCM/NoPadding` 128-bit, `EncryptedSharedPreferences`-like (cipher+iv Base64 in `pos_agent_secure`), `save/load/clear`, tampered → null (treated missing). Mirrors Windows DPAPI.
- **Android Keystore:** `KeyStore.getInstance("AndroidKeyStore")`, `KeyGenerator AES` with `KeyGenParameterSpec BLOCK_MODE_GCM`, `ENCRYPTION_PADDING_NONE`.
- **Token transmission:** `Authorization: Bearer <token>` header at runtime only (PrintAgentService HttpTransport); HTTPS enforced (`https://` check, `usesCleartextTraffic=false`); cert validation ON (no trust-all).
- **Logging:** `FileLog` + `PrintPipeline.Log` — event/status/job/http/duration/error only; token/header/body never logged (explicit in code comments + FileLog safeError).
- **Persisted credentials:** server URL, printer MAC (`pos_agent` prefs, MODE_PRIVATE) + encrypted token; branch/destination NOT persisted (server-authoritative per claim).
- **NOT persisted:** plaintext token, branch_id, destination, print payload.

## 7. Current Kitchen identity model

**Actual architecture (from source):**
- Identity = **device token** (raw string, generated server-side via `POST /devices/{id}/token`, shown once, SHA-256 stored server, Keystore on device).
- `MainActivity` stores server URL + token (Keystore) + printer MAC; token input is write-once (hint “tersimpan — biarkan kosong” after save).
- `PrintAgentService` loads token via `SecureTokenStore.load(this)` at loop start; missing → ERROR + stopSelf (never proceeds without token).
- `HttpTransport` sends `Authorization: Bearer <token>` — server `DeviceService::authenticate` resolves device row + branch.

**Not assumed:**
- `Bluetooth MAC ≠ Device ID` — MAC is printer selection only (`BluetoothPrinter`), never device identity.
- `device token ≠ Bluetooth credential` — token is HTTPS auth; Bluetooth uses paired-device trust (no token).

## 8. Destination model

**Server-side, device-registration-based, token-based (inferred from server code, not Android alone):**
- Android source: `PrintAgentService`/`PrintPipeline`/`DeviceApiProtocol` — **zero references** to `KITCHEN`/`RECEIPT`/`destination` filtering (grep: none in `src/agent/android/PrintAgentService.java` / `PrintPipeline.java`). The agent claims “whatever the server gives for this token.”
- Server code (authoritative, inspected in prior phases): `DeviceApiController::DESTINATION_BY_TYPE` maps `PRINTER_KITCHEN → KITCHEN`, `KITCHEN_PRINT_BRIDGE → KITCHEN`, `PRINTER_KASIR → RECEIPT`; `DeviceApiController::claimJob` derives `destination` from `device.type` + `branch_id` from device row; client-supplied branch/destination **ignored by design** (“Do NOT assume filenames beyond known files” — actual endpoint enforces it).
- **Conclusion for Android:** **NOT DETERMINABLE FROM ANDROID SOURCE ALONE** for the mapping table — identified server file is `app/Modules/Device/DeviceApiController.php` (and `PrintJobService::claim`). Android reuse for RECEIPT requires only a new device row of type `PRINTER_KASIR` (branch kasir) — no APK code change, no destination hard-code.

---
PHASE 0 STATUS: PASS — reference project fully understood (structure, build, runtime, server, Bluetooth, rendering, security, identity, destination).
