diff --git a/docs/superpowers/specs/2026-08-24-lan-absdk-and-local-experience-design.md b/docs/superpowers/specs/2026-08-24-lan-absdk-and-local-experience-design.md new file mode 100644 index 0000000..63e82bb --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-lan-absdk-and-local-experience-design.md @@ -0,0 +1,245 @@ +# LAN ABSDK and Local Experience Design + +## Status + +Approved on 2026-08-24 by the project owner. This specification replaces the three-provider model in `feat/provider-login`. + +## Goal + +ABox Manager exposes exactly two choices before authentication: + +1. **ABox** uses the real vendor ABSDK bridge to communicate with the smart-home gateway available on the current LAN. +2. **Local experience** uses production-safe simulation implementations, establishes a simulated session immediately, and enters the authenticated app without requesting credentials. + +The app no longer offers or plans a self-hosted ABSDK-compatible backend. + +## Confirmed Environment and Constraints + +- The observed gateway address is `192.168.100.1`. +- The gateway responds to the ABSDK-compatible route `POST /abox/index.php/abjhsdk/user/validate`. +- `libABSDK0630.jar` hardcodes `a-box.com.cn/abox/index.php/` and does not expose a supported base-URL setter. +- `setHostInfo(url, hotelName, roomNo, apikey)` submits `url` as an initialization form field and does not change the HTTP target. +- Hardware testing on `feat/moler-infrared-control` confirms that the current network environment allows the vendor ABSDK to reach the real gateway. +- Debug and release builds expose the same provider choices and routing behavior. Release is not treated as a special case. +- The vendor JAR uses cleartext HTTP. The existing `@CleartextAbsdkTransport` opt-in remains mandatory, and the Android application must explicitly permit the required LAN transport. +- Fake and real sessions, devices, state, and credentials must never be mixed. + +## Provider Model + +`ProviderType` contains only: + +```kotlin +enum class ProviderType { + ABOX, + LOCAL_EXPERIENCE, +} +``` + +The built-in profiles are: + +```text +abox-default -> ProviderType.ABOX +local-experience -> ProviderType.LOCAL_EXPERIENCE +``` + +The persisted profile store remains tolerant of removed enum values. A stored `COMPATIBLE_BACKEND` profile is ignored. If it was active, the app returns to provider selection rather than attempting migration to another provider. + +## User Flow + +### ABox + +```text +Provider selection + -> choose ABox + -> save and activate ABox profile + -> restore automatic-login preferences when available + -> otherwise show username/password login + -> SessionCoordinator delegates to the process-wide AbsdkBridge + -> enter authenticated app only after DomainResult.Success +``` + +Invalid credentials, network errors, and token failures remain on the login screen and display localized errors. + +### Local experience + +```text +Provider selection + -> choose Local experience + -> save and activate local profile + -> SimulationSessionGateway establishes Username("local") session + -> enter authenticated app immediately +``` + +Local experience: + +- never displays the credential form; +- never calls ABSDK; +- never reads or writes Keystore credentials; +- uses production-safe simulation device gateways; +- can later be removed as a single provider/module boundary if the team decides not to ship it. + +## Module Boundaries + +### `:absdk-bridge` + +Remains the anti-corruption layer around the vendor JAR and implements: + +- `SessionGateway`; +- `SocketGateway`; +- `TemperatureHumidityGateway`; +- `InfraredGateway`. + +A single process-wide `AbsdkBridge` instance is shared by session and device operations so the mutex and vendor singleton Token remain aligned. + +### New `:runtime:simulation` + +A production Kotlin/JVM module implementing: + +- `LocalSessionGateway` as `SimulationSessionGateway`; +- `DeviceCatalog` as `SimulationDeviceCatalog`; +- `SocketGateway` as `SimulationSocketGateway`; +- `TemperatureHumidityGateway` as `SimulationTemperatureHumidityGateway`; +- `InfraredGateway` as `SimulationInfraredGateway`. + +These implementations provide deterministic local experience behavior but do not expose test-only failure injection, mutable call-recording APIs, or assertion helpers. + +### `:testing:fakes` + +Remains test-only. It keeps scriptable errors, call recording, and other testing controls. It must not be added as an application runtime dependency. + +### `:android:core:session` + +Keeps provider/profile persistence, Keystore credential storage, and `LocalSessionGatewayImpl` only until the new simulation gateway replaces it. After migration, the old local gateway implementation is removed to prevent two production local-session implementations. + +### `:android:app` + +Owns the Hilt composition root and provider-aware routing. It injects: + +```text +SessionGateway -> AbsdkBridge +LocalSessionGateway -> SimulationSessionGateway +``` + +For device operations, the app exposes provider-routing adapters: + +```text +RoutingDeviceCatalog +RoutingSocketGateway +RoutingTemperatureHumidityGateway +RoutingInfraredGateway +``` + +Each adapter observes or reads the active `ProviderProfile` and delegates: + +```text +ABOX -> real AbsdkBridge implementation +LOCAL_EXPERIENCE -> simulation implementation +``` + +No feature UI selects Real or Fake directly. + +## Session and Credential Rules + +- `SessionCoordinator` continues to select the session gateway from `ProviderType`. +- ABox credentials can use remember-username and automatic-login settings. +- Automatic-login passwords are stored through Android Keystore. +- Local experience cannot persist credentials or login preferences requiring a password. +- Switching provider logs out the current gateway before activating another profile. +- Logging out of ABox clears credentials and the Bridge-local session but leaves the ABox profile active so the login page can be shown again. +- Logging out of local experience clears the local session and active profile, returning to provider selection. +- Only a session whose `profileId` matches the active profile can produce the authenticated UI state. + +## Provider Availability + +`ProviderAvailability` is simplified to the two supported options. Both are enabled in all build types: + +```kotlin +data class ProviderAvailability( + val abox: Availability = Availability.Enabled, + val local: Availability = Availability.Enabled, +) +``` + +`Availability.Simulation` and compatible-backend-specific disabled reasons are removed unless another current UI requirement uses them. + +## Android Transport + +The application declares: + +```xml + +``` + +and permits the vendor cleartext transport required by the current ABSDK. This is an explicit acceptance of the current LAN integration constraint, not a claim that the transport is encrypted. + +No password, Token, API key, raw vendor response, or infrared payload may be logged. + +## UI and Copy + +Provider selection presents exactly: + +- **ABox** — connects to the ABox smart-home gateway through ABSDK on the current LAN; +- **Local experience** — uses simulated local devices without network access. + +All compatibility-backend copy, status labels, and branches are removed from English and Simplified Chinese resources. + +## Error Handling + +- ABox authentication failure maps to the localized invalid-credentials message and does not create a session. +- Network failure maps to the localized network message and does not create a session. +- Token invalidation clears the Bridge-local session and returns the app to login. +- Selecting local experience fails visibly if profile persistence or simulated-session creation fails. +- Routing adapters return `DomainError.InvalidRequest` when no active profile exists or the profile type is unsupported. +- Cancellation is rethrown rather than converted to a normal failure. + +## Testing Strategy + +### Unit tests + +- `ProviderType` and provider selection expose exactly ABox and local experience. +- Removed persisted compatible profiles are ignored and cannot become active. +- `SessionCoordinator` routes ABox to `SessionGateway` and local experience to `LocalSessionGateway`. +- Arbitrary ABox credentials do not succeed unless the injected real/fake test gateway returns success. +- Local experience selection creates a session without calling the ABox gateway or credential store. +- Each routing adapter delegates ABox calls to the real test double and local calls to simulation. +- Switching providers logs out the previous session and isolates state. + +### Integration and build verification + +- Debug and release compile with the same real ABSDK and simulation modules. +- Hilt resolves one binding for each public gateway. +- Android lint validates Internet/cleartext configuration. +- Full command: `./gradlew check testDebugUnitTest test`. + +### Authorized hardware validation + +On a device connected to the gateway LAN: + +1. Choose ABox. +2. Enter a known-invalid credential pair and verify the app stays on login. +3. Enter an authorized valid credential pair and verify authenticated navigation. +4. Validate Token using the shared Bridge. +5. Perform one read-only device query before any control operation. +6. Choose local experience and verify no ABSDK call or network dependency is required. + +Sensitive credentials and Tokens are never captured in test reports. + +## Documentation Changes + +The implementation updates: + +- `docs/requirement_spec.md`; +- `docs/project_plan.md`; +- `docs/architecture.md`; +- `docs/protocol/absdk-0630.md`. + +The documents must no longer state that the product connects to an ABox cloud service or a self-hosted compatible backend. They must distinguish confirmed LAN behavior from the JAR's still-hardcoded host behavior. + +## Out of Scope + +- Reimplementing the ABSDK HTTP/signature protocol; +- patching or rewriting the vendor JAR; +- adding a self-hosted compatibility server; +- adding an in-app gateway IP field that the current JAR cannot consume; +- merging the authenticated app shell PR; +- deciding whether local experience remains in the final product beyond preserving it as a removable module boundary.