feat: add provider login flow (#4)

## Summary

- replace the former three-provider model with two pre-login choices:
  - **ABox**: authenticate through the vendor ABSDK on the current local network
  - **Local experience**: establish a production simulation session without credentials, network access, or ABSDK calls
- remove the compatible-backend product/server scope and safely ignore legacy persisted compatible-provider values
- bind one process-wide `AbsdkBridge` for ABox session, socket, temperature/humidity, and infrared operations
- add production `:runtime:simulation` implementations and route device operations by the active provider
- retain provider-scoped username preferences, Keystore-encrypted automatic-login credentials, token recovery, logout, and session isolation
- add regression coverage for authentication gating, profile/session alignment, provider switching cleanup, routing, and simulation behavior
- align requirement, plan, architecture, and protocol documentation with LAN ABSDK access

## Verification

```text
./gradlew check testDebugUnitTest test
BUILD SUCCESSFUL (220 actionable tasks)

./gradlew :android:app:assembleDebug :android:app:assembleRelease
BUILD SUCCESSFUL (217 actionable tasks)

./gradlew :android:app:compileDebugAndroidTestKotlin
BUILD SUCCESSFUL

git diff --check
passed
```

Emulator smoke validation:

- provider selection displays ABox and Local experience only
- Local experience enters the authenticated route without credentials
- Local experience also enters while emulator Wi-Fi/data are disabled
- Local Sign out and Switch provider return to provider selection
- ABox opens the credential form; empty submission stays on login with the required-fields error

## Known constraints and accepted risks

- The vendor JAR uses cleartext HTTP and hardcodes `a-box.com.cn`; Debug and Release intentionally share the same ABox capability under the approved no-Release-exception decision. `@CleartextAbsdkTransport`, `usesCleartextTraffic`, and the architecture/protocol documents explicitly record this risk. This does not provide confidentiality or integrity.
- Real authenticated ABox and read-only hardware smoke validation is deferred because the gateway is currently unavailable locally. This PR does not claim that current-environment validation completed.
- The mapping from the vendor JAR's hardcoded hostname to the LAN gateway remains an external network/deployment prerequisite. The ABSDK JAR and bridge path remain identical to the previously hardware-validated `feat/moler-infrared-control` branch.
- `FakeDeviceCatalog` remains a temporary debug-only placeholder until the device-catalog module provides the production ABox catalog. ABox catalog requests are not routed to Local experience simulation.

Reviewed-on: #4
Co-authored-by: KremeCN <3265494849@qq.com>
This commit was merged in pull request #4.
This commit is contained in:
2026-08-25 01:59:00 +08:00
committed by KremeCN
parent 6f7d3248b0
commit 06ccfdf952
59 changed files with 4982 additions and 209 deletions
+72 -24
View File
@@ -2,7 +2,7 @@
## 1. 文档范围
本文记录第一轮工程基础已经建立的模块边界、依赖方向和并行开发接口。ABSDK 0630 的初始化、协议、签名、连接重定向和会话清理等未确认内容不在本文中固化。
本文记录已建立的模块边界、依赖方向、提供方路由和并行开发接口,覆盖 ABox(局域网 ABSDK)与本地体验两条连接路径。ABSDK 0630 的初始化、签名细节和会话清理等未确认内容不在本文中固化,协议观测证据见 `docs/protocol/absdk-0630.md`。
产品范围和固定分工以 `docs/project_plan.md` 为准,原始简要需求继续保留在 `docs/requirement_spec.md`。
@@ -11,10 +11,14 @@
```text
abox-mgr/
├── android/
│ ├── app/ # Android 应用与依赖注入组合根
│ └── core/designsystem/ # Compose Material 3 设计系统基础
│ ├── app/ # Android 应用、提供方路由与依赖注入组合根
│ └── core/
│ ├── designsystem/ # Compose Material 3 设计系统基础
│ └── session/ # 提供方 profile、凭据 Keystore 持久化
├── domain/ # 纯 Kotlin 领域模型和公共端口
├── testing/fakes/ # 确定性的公共 Fake
├── absdk-bridge/ # ABSDK 0630 防腐层(生产)
├── runtime/simulation/ # 本地体验模拟运行时(生产)
├── testing/fakes/ # 测试专用 Fake(仅测试依赖)
├── docs/
└── samples/ # 历史示例,仅供参考
```
@@ -23,10 +27,13 @@ abox-mgr/
```text
android/app ────────────────┬──> android/core/designsystem
├──> android/core/session ──> domain
├──> domain
└──> testing/fakes ──> domain
├──> absdk-bridge ──> domain
└──> runtime/simulation ──> domain
android/core/designsystem ─────> Compose Material 3
testing/fakes(仅测试)────────> domain
domain ────────────────────────> Kotlin 与 Coroutines
```
@@ -34,21 +41,29 @@ domain ────────────────────────>
## 3. 提供方与数据隔离
应用计划支持 ABox、自有兼容后端和本地体验三种提供方。每个连接配置由 `ProviderProfileId` 标识,所有会话、设备目录和设备操作都显式携带该标识。
认证前应用只提供两种入口,两者在所有构建变体中始终可用:
1. **ABox**:通过进程内唯一的真实 `AbsdkBridge` 连接当前局域网中的智能家居网关,登录成功后进入应用;
2. **本地体验**:由 `:runtime:simulation` 直接建立模拟会话并进入应用,不请求凭据、不访问网络、不调用 ABSDK。
每个提供方 profile 由 `ProviderProfileId` 标识,所有会话、设备目录和设备操作都显式携带该标识。
约束:
- 任一时刻只启用一个提供方;
- 切换远端提供方前必须退出当前会话;
- 不同连接配置的数据、凭据和设备状态不得互相混用;
- 切换提供方前必须退出当前会话;
- 不同提供方的数据、凭据和设备状态不得互相混用;
- 本地体验不调用 ABSDK,也不启动本地 HTTP 服务;
- Fake 使用相同标识隔离规则,便于在没有真实环境时验证边界。
- Release 不是特殊情况:debug 与 release 暴露相同的提供方和路由行为。
## 4. 公共领域端口
`domain` 当前提供以下最小端口:
- `SessionGateway`:登录、观察会话、验证会话和退出;
- `LocalSessionGateway`:无凭据本地会话的标记端口,继承 `SessionGateway`;
- `ProviderProfileStore`:提供方 profile、活动 profile 和登录偏好持久化;
- `CredentialStore`:自动登录凭据的加密存取;
- `DeviceCatalog`:按提供方观察设备目录;
- `SocketGateway`:查询和设置插座状态;
- `TemperatureHumidityGateway`:读取温度和湿度;
@@ -63,9 +78,9 @@ domain ────────────────────────>
- `Success<T>` 携带类型化领域值;
- `Failure` 携带 `DomainError`;
- `DomainError.externalCode` 可以保留外部 SDK 错误码用于诊断;
- UI 不接触 `ABRet`、原始 `Map`、HTTP 表单或服务端数据库模型。
- UI 不接触 `ABRet`、原始 `Map`、HTTP 表单或任何服务端数据库模型。
真实 ABSDK 适配层必须在后续 `feat/absdk-bridge` 分支中完成同步阻塞调用的后台串行化、返回解析和错误映射。
ABSDK 防腐层已在 `:absdk-bridge` 中完成同步阻塞调用的后台串行化、返回解析和错误映射。
## 6. Fake 边界
@@ -77,42 +92,75 @@ domain ────────────────────────>
- 记录红外学习、发送和下载调用;
- 可直接用于其他模块的 ViewModel 与 UI 测试。
Fake 只用于开发和测试,不能作为真实 ABox 或真实硬件验收证据。
Fake 只用于开发和测试,不能作为真实 ABox 或真实硬件验收证据。`:testing:fakes` 是测试专用模块,不得加入应用生产依赖。
## 7. Android 组合根
一个例外是 `FakeDeviceCatalog`:ABSDK 不提供设备目录,而提供方作用域的生产 ABox 设备目录由设备目录模块负责,尚未落地。在该模块交付前,debug 构建的组合根暂时用 `FakeDeviceCatalog` 充当 ABox 目录占位。这是一个临时的目录所有权边界:不得把 ABox 目录请求路由到本地体验的模拟目录,release 构建在出现真实消费方之前不声明 `DeviceCatalog` 绑定。
## 7. 本地体验模拟运行时
`:runtime:simulation` 是生产 Kotlin/JVM 模块,为本地体验提供确定性实现:
- `SimulationSessionGateway`:直接建立无凭据的本地会话;
- `SimulationDeviceCatalog`:内置的本地体验设备目录;
- `SimulationSocketGateway`、`SimulationTemperatureHumidityGateway`、`SimulationInfraredGateway`:固定行为的模拟设备操作。
模拟实现不暴露脚本化失败注入、调用记录或断言辅助;这些能力只存在于 `:testing:fakes`。
## 8. Android 组合根与提供方路由
`android/app` 是单 Activity Compose 应用,负责:
- `ABoxManagerApplication` 与 Hilt 组合根;
- `MainActivity`;
- 根 `NavHost`;
- 仅在 debug 构建中将领域端口绑定到当前开发阶段的 Fake,release 构建不包含 Fake;
- 根 `NavHost` 与提供方选择、登录、认证后路由;
- 默认英文和简体中文资源;
- 使用 `android/core/designsystem` 的主题和基础组件。
组合根按端口绑定生产实现:
```text
SessionGateway -> AbsdkBridge(进程内单例)
LocalSessionGateway -> SimulationSessionGateway
SocketGateway -> RoutingSocketGateway
TemperatureHumidityGateway -> RoutingTemperatureHumidityGateway
InfraredGateway -> RoutingInfraredGateway
```
`AbsdkBridge` 以 Hilt 单例提供并在所有构建变体中相同:会话与插座/温湿度/红外操作共享同一个 Bridge 实例,使互斥锁和 Bridge 本地会话与厂商 SDK 单例状态保持一致。三个 Routing 网关读取活动 profile 并按提供方分发——ABox 转发到共享的 AbsdkBridge 实现,本地体验转发到模拟实现。任何功能 UI 都不直接选择真实或 Fake 实现。
设备目录导航、插座与温湿度页面、红外页面和情景页面由固定模块负责人实现,不放入工程基础层。
## 8. ABSDK Bridge
## 9. ABSDK Bridge
`:absdk-bridge` 是纯 Kotlin/JVM 防腐层,依赖 `domain` 并实现会话、插座、温湿度和红外 Gateway。该模块:
- 只在 `VendorAbsdkClient` 中接触 `ABSDK`、`ABRet` 和 `IrInfo`;
- 将所有同步调用移到后台并串行进入全局 SDK;
- 将返回值和错误码转换为 `DomainResult` 和领域模型;
- 不提供设备目录;
- 不调用未确认的 `setHostInfo`;
- logout 只清除 Bridge 本地会话,因为 JAR 没有公开 logout 或 Token setter;
- 生产工厂始终返回进程内同一个 Bridge,使互斥锁和本地会话与厂商单例保持一致;
- 真实工厂标记为 `@CleartextAbsdkTransport`,调用方必须显式 opt-in 承认厂商 JAR 的硬编码 HTTP 风险;
- 当前直接引用归档 Sample 目录中的唯一一份 JAR,不复制厂商二进制。
- 当前直接引用归档 Sample 目录中的唯一一份 JAR,不复制厂商二进制;
- ABSDK 0630 没有设备目录能力,Bridge 因此也不提供设备目录或设备发现。
Bridge 本轮尚未加入 `android/app` 依赖,App debug 仍绑定 Fake。提供方选择、安全凭据和真实 Bridge 绑定由后续 `feat/provider-login` 完成;在获批的传输控制方案明确前,不得在 App 中启用真实 Bridge。协议证据与限制记录在 `docs/protocol/absdk-0630.md`。
App 组合根已通过 Hilt 单例绑定该 Bridge,绑定处以 `@CleartextAbsdkTransport` 显式 opt-in 承认厂商 JAR 的硬编码明文 HTTP 风险;App 同时声明 `INTERNET` 权限并允许明文传输(`usesCleartextTraffic`),这是项目所有者对当前局域网集成约束的明确风险接受,不代表传输具备机密性或完整性。协议证据与限制记录在 `docs/protocol/absdk-0630.md`。
## 9. 后续边界
## 10. Provider Session
后续 KremeCN 工作保持为独立短期分支:
`:android:core:session` 负责提供方登录的持久化与 Android 安全边界:
- `feat/provider-login`:提供方入口、安全凭据、自动登录、重新登录和真实 Bridge 绑定;
- `feat/compatible-server`:在协议与连接方式确认后建立服务端、CLI 和 Docker 基础。
- `ProviderProfileStore` 使用 DataStore 保存 profile、当前活动 profile、记住用户名和自动登录开关;已移除的提供方类型(如历史兼容后端值)被忽略,不会成为活动 profile;
- `CredentialStore` 使用 Android Keystore 中的 AES-256-GCM 密钥加密自动登录凭据,DataStore 只保存带随机 IV 的密文;
- `ProviderProfileId` 作为 GCM AAD,防止不同 profile 的密文互换;
- 本地体验的会话由 `:runtime:simulation` 的 `SimulationSessionGateway` 提供,不调用 HTTP 或 ABSDK。
Room、DataStore 和 Android Keystore 的具体 schema/key 只在对应数据所有权明确后创建。历史示例中的 AsyncTask、明文密码存储、全局明文网络和散落 SDK 调用不得进入正式架构。
纯 Kotlin `SessionCoordinator` 统一处理启动恢复、提供方选择、登录偏好、自动登录、Token 失效重登、退出和切换提供方。ABox 登录成功必须来自注入的真实 Bridge 返回 `DomainResult.Success<Session>`;无效凭据和网络错误停留在登录页并显示本地化错误。ABox 远端退出清除密码和 Session,但保留 profile 与记住的用户名以便再次显示登录页;本地体验退出清除本地会话和活动 profile,回到提供方选择。
两个提供方在所有构建变体中始终可用,不存在按 debug/release 区分的可用性开关。
登录完成后进入 `authenticated` 协作路由。当前仅有占位页面,设备列表与主导航由 alZerNest 的设备导航模块接管。
## 11. 后续边界
Room 与设备业务 DataStore 的具体 schema/key 只在对应数据所有权明确后创建。生产 ABox 设备目录由设备目录模块交付后替换 debug 构建中的临时 `FakeDeviceCatalog` 占位。历史示例中的 AsyncTask、明文密码存储、全局明文网络和散落 SDK 调用不得进入正式架构。