# PHASE X4.x — ANDROID BLUETOOTH STALE-SOCKET RECOVERY

## Status

**REAL DEVICE PASS** — PHASE CLOSED — IMPLEMENTATION FROZEN

## Scope

Hardening lifecycle koneksi Bluetooth pada `agent/android/BluetoothPrinter.java`
saja (+ test deterministik `test-android/`). Tanpa perubahan server, database,
PrintPipeline, renderer, manifest, protokol claim/lease/ACK.

## Root Cause

`BluetoothSocket.isConnected()` hanya merefleksikan state socket lokal — RFCOMM
tidak memberikan liveness guarantee. Koneksi yang mati diam-diam (printer off /
out-of-range / link drop) tetap terlihat `connected=true` sampai write
berikutnya gagal `Broken pipe` (terbukti: job 162). Android tidak punya
mekanisme deteksi liveness tanpa mengirim byte ke printer, dan pengiriman
byte probe dilarang (akan tercetak sebagai sampah).

## Implemented Fix

Dua lapisan (keduanya di `BluetoothPrinter.java`):

1. **Stale-idle heuristic (X4.x-FIX):** `STALE_CONNECTION_MAX_IDLE_MS = 60_000`
   + baseline `lastSuccessfulWriteMs` (dicatat saat write+flush sukses). Saat
   write berikutnya tiba dan gap idle > 60 detik → log
   `WRITE STALE_RECONNECT reason=idleTooLong` → close → reconnect SEKALI
   sebelum write pertama. Hanya terjadi setelah idle panjang (bukan sebelum
   setiap print); tanpa timer/polling/ping; clock-backward TIDAK pernah
   dianggap stale; koneksi baru tanpa baseline TIDAK pernah dicurigai.
2. **Write-failure recovery (existing, field-proven, TIDAK diubah):**
   IOException/Broken pipe → close → reconnect (secure → insecure fallback)
   → retry payload SAMA tepat satu kali (`MAX_WRITE_ATTEMPTS = 2`). Tidak
   ada penulisan payload ganda di luar satu retry terdokumentasi.

## Test Evidence

- **9/9 deterministic JVM decision tests PASS** (`test-android/`,
  `agent.android.RunBluetoothTests`; null/empty payload, batas stale
  59s/60s/61s, fresh-never-stale, clock-skew, idle arithmetic).
- **APK build PASS:** `android-print-agent/build/outputs/apk/debug/pos-print-agent-debug.apk`
  (119,841 bytes, 2026-10-07 17:26, BUILD SUCCESSFUL).
- **Real-device validation (serial `6X8T8DRK5HY9GQEA`, printer RPP02N
  `86:67:7A:11:1C:A7`):**
  - JOB 162: write awal `Broken pipe` → close → reconnect SECURE → retry →
    write success → ACK HTTP 200 → PRINTED (recovery field-proven).
  - JOB 163: existing connection → write success → ACK 200 → PRINTED.
  - JOB 170: new secure connection → write success → ACK 200 → PRINTED.
  - JOB 173: existing connection → write success → ACK 200 → PRINTED.
  - **JOB 182 (CRITICAL stale-idle acceptance):**
    `WRITE STALE_RECONNECT reason=idleTooLong idleMs=136004 thresholdMs=60000`
    → `CLOSE mode=SECURE` → `CONNECT SUCCESS mode=SECURE` →
    `WRITE SUCCESS` → `PRINT_RESULT SUCCESS` → `ACK HTTP 200` →
    `CYCLE_END PRINTED`.
  - JOB 183: existing connection setelah stale recovery → write success →
    ACK 200 → PRINTED (subsequent job sehat).

## Acceptance Matrix

| Criterion | Evidence | Status |
|---|---|---|
| APK build succeeded | BUILD SUCCESSFUL 17:26 | PASS |
| 9/9 deterministic JVM decision tests | RunBluetoothTests 9/9 | PASS |
| Real Android device validation completed | jobs 162/163/170/173/182/183 | PASS |
| New Bluetooth connection path verified | job 170 (new SECURE connect) | PASS |
| Existing connection path verified | jobs 163, 173 | PASS |
| Write-failure recovery verified | job 162 (Broken pipe → reconnect → retry) | PASS |
| Idle >60s stale detection verified | job 182 (idleMs=136004 > 60000) | PASS |
| Stale connection reconnect verified | job 182 (CLOSE → CONNECT SUCCESS → WRITE SUCCESS) | PASS |
| Same payload written successfully after reconnect | job 182 (single retry semantics; write success) | PASS |
| ACK HTTP 200 verified | semua job | PASS |
| Subsequent job after stale recovery verified | job 183 | PASS |
| No job loss observed | semua job PRINTED + ACK | PASS |
| No duplicate print observed | satu retry terdokumentasi, tanpa double-write | PASS |
| No server/database changes required | SERVER CHANGE: NONE | PASS |

## Files Changed (oleh X4.x)

- `android-print-agent/src/agent/android/BluetoothPrinter.java`
- `android-print-agent/test-android/BluetoothPrinterTest.java` (NEW)
- `android-print-agent/test-android/RunBluetoothTests.java` (NEW)

## Files Intentionally Not Changed

- `PrintAgentService.java`
- `PrintPipeline.java`
- Renderer (`EscPosRenderer.java`)
- `AndroidManifest.xml`
- Server (PHP/Device API)
- Database / migrations
- Windows Print Bridge

## Known Limitations

1. Heuristic berbasis idle-time tidak dapat mendeteksi socket yang mati
   PERSIS saat write — jendela itu tetap ditangani retry IOException
   (job 162) yang field-proven. Tidak ada deteksi liveness tanpa probe bytes.
2. Retry satu kali setelah kegagalan transport secara teoretis dapat
   menghasilkan duplikat cetak bila printer sempat menerima byte sebelum
   error — at-least-once, didokumentasikan, identik dengan Windows bridge.
3. Core JVM suite (48 tes X4.24) tidak runnable di environment tanpa
   Android runtime sejak `PrintPipeline` diagnostik mengimpor
   `android.util.Log` — di luar scope X4.x; kompilasi terhadap android.jar
   terbukti lewat build APK.

## Follow-up Candidates (TIDAK diimplementasikan)

- Threshold 60s bisa dijadikan konfigurasi UI (saat ini konstanta) bila
  pola idle outlet berbeda.
- Rekonstruksi core JVM suite yang dapat jalan tanpa android.jar
  (memisahkan logging diagnostik dari logika murni) — fase terpisah.

## Final Decision

**REAL DEVICE PASS — PHASE CLOSED — IMPLEMENTATION FROZEN**
