## 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>
1060 lines
42 KiB
Markdown
1060 lines
42 KiB
Markdown
# LAN ABSDK and Local Experience Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace the three-provider simulation login flow with two pre-login choices: real ABox ABSDK authentication on the LAN and a production-safe local experience that creates a simulated session immediately.
|
|
|
|
**Architecture:** `SessionCoordinator` keeps routing by `ProviderType`: `ABOX` delegates to the process-wide real `AbsdkBridge`, while `LOCAL_EXPERIENCE` delegates to a new production `:runtime:simulation` module. Socket, temperature/humidity, and infrared calls use provider-aware routing adapters; ABSDK device discovery is not invented, so the ABox device catalog remains a provider-scoped local persistence responsibility owned by the device-catalog module.
|
|
|
|
**Tech Stack:** Kotlin 1.9.24, Coroutines/Flow, Android API 26+, Jetpack Compose Material 3, Hilt, Preferences DataStore, Android Keystore, JUnit 4, AndroidX Compose UI tests, vendor `libABSDK0630.jar`.
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-08-24-lan-absdk-and-local-experience-design.md`
|
|
|
|
## Global Constraints
|
|
|
|
- The provider selection screen exposes exactly `ABOX` and `LOCAL_EXPERIENCE`; `COMPATIBLE_BACKEND` is removed.
|
|
- ABox uses the same process-wide `AbsdkBridge` instance for session, socket, temperature/humidity, and infrared operations in debug and release.
|
|
- Local experience creates `Username("local")` without a credential page, Keystore access, ABSDK call, or network dependency.
|
|
- `testing:fakes` remains test-only and is never an application runtime dependency.
|
|
- Production simulation implementations do not expose failure injection or mutable call-recording APIs.
|
|
- The Android app targets API 34, supports API 26+, and explicitly opts into the vendor JAR's required cleartext LAN transport.
|
|
- No password, Token, API key, raw vendor response, or infrared payload may be logged.
|
|
- `DeviceCatalog` is not implemented by ABSDK 0630; do not add a remote discovery assumption.
|
|
- Every task follows red-green-refactor TDD and ends with an independently reviewable commit.
|
|
- Do not merge or modify PR #6 while executing this plan.
|
|
|
|
---
|
|
|
|
## File and Responsibility Map
|
|
|
|
### New files
|
|
|
|
- `runtime/simulation/build.gradle.kts` — production Kotlin/JVM simulation module configuration.
|
|
- `runtime/simulation/src/main/kotlin/com/flagship/abox/manager/simulation/SimulationSessionGateway.kt` — local-experience session lifecycle.
|
|
- `runtime/simulation/src/main/kotlin/com/flagship/abox/manager/simulation/SimulationDeviceCatalog.kt` — deterministic built-in local-experience devices.
|
|
- `runtime/simulation/src/main/kotlin/com/flagship/abox/manager/simulation/SimulationDeviceGateways.kt` — production socket, temperature/humidity, and infrared simulation behavior.
|
|
- `runtime/simulation/src/test/kotlin/com/flagship/abox/manager/simulation/SimulationSessionGatewayTest.kt` — session contract tests.
|
|
- `runtime/simulation/src/test/kotlin/com/flagship/abox/manager/simulation/SimulationDeviceGatewaysTest.kt` — local device behavior and profile-isolation tests.
|
|
- `android/app/src/main/kotlin/com/flagship/abox/manager/di/ProviderRoutingGateways.kt` — active-provider routing for ABSDK-backed device operations.
|
|
- `android/app/src/test/kotlin/com/flagship/abox/manager/di/ProviderRoutingGatewaysTest.kt` — routing tests using test doubles.
|
|
- `android/app/src/test/kotlin/com/flagship/abox/manager/testing/MainDispatcherRule.kt` — deterministic `Dispatchers.Main` replacement for ViewModel unit tests.
|
|
- `android/app/src/test/kotlin/com/flagship/abox/manager/ui/provider/ProviderSelectionViewModelTest.kt` — exactly-two-provider behavior and local direct-entry tests.
|
|
- `android/core/session/src/test/kotlin/com/flagship/abox/manager/session/RemovedProviderProfileTest.kt` — stale compatible-profile persistence behavior.
|
|
|
|
### Modified files
|
|
|
|
- `settings.gradle.kts` — includes `:runtime:simulation`.
|
|
- `domain/src/main/kotlin/com/flagship/abox/manager/domain/Models.kt` — removes `COMPATIBLE_BACKEND`.
|
|
- `domain/src/main/kotlin/com/flagship/abox/manager/domain/SessionCoordinator.kt` — retains only ABox/local routing and enforces local credential isolation.
|
|
- `testing/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/SessionCoordinatorTest.kt` — verifies real/local delegation and no credential use for local experience.
|
|
- `android/core/session/src/main/kotlin/com/flagship/abox/manager/session/profile/ProviderProfileStoreImpl.kt` — ignores removed persisted provider values and clears stale active selection behavior.
|
|
- `android/core/session/src/main/kotlin/com/flagship/abox/manager/session/gateway/LocalSessionGatewayImpl.kt` — deleted after the simulation implementation replaces it.
|
|
- `android/app/build.gradle.kts` — adds runtime `:absdk-bridge` and `:runtime:simulation`, removes runtime `testing:fakes`, and adds packaging exclusions for legacy JAR metadata.
|
|
- `android/app/src/main/AndroidManifest.xml` — adds Internet permission and cleartext transport opt-in required by ABSDK.
|
|
- `android/app/src/main/kotlin/com/flagship/abox/manager/di/SessionModule.kt` — binds the real Bridge and simulation session gateway uniformly for every build type.
|
|
- `android/app/src/debug/kotlin/com/flagship/abox/manager/di/DebugSessionModule.kt` — deleted.
|
|
- `android/app/src/release/kotlin/com/flagship/abox/manager/di/ReleaseSessionModule.kt` — deleted.
|
|
- `android/app/src/debug/kotlin/com/flagship/abox/manager/di/FoundationModule.kt` — removes runtime fake session/device bindings superseded by uniform routing.
|
|
- `android/app/src/main/kotlin/com/flagship/abox/manager/ui/provider/ProviderAvailability.kt` — reduces availability to two enabled providers.
|
|
- `android/app/src/main/kotlin/com/flagship/abox/manager/ui/provider/ProviderSelectionViewModel.kt` — exposes exactly ABox and local experience.
|
|
- `android/app/src/main/kotlin/com/flagship/abox/manager/ui/provider/ProviderSelectionScreen.kt` — removes compatible/simulation/disabled branches.
|
|
- `android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt` — removes compatible-backend text branches.
|
|
- `android/app/src/main/res/values/strings.xml` — updates ABox LAN copy and removes compatible-provider strings.
|
|
- `android/app/src/main/res/values-zh-rCN/strings.xml` — matching Simplified Chinese copy.
|
|
- `android/app/src/androidTest/kotlin/com/flagship/abox/manager/ui/provider/ProviderSelectionScreenTest.kt` — verifies both supported options are selectable and no third option renders.
|
|
- `docs/requirement_spec.md`, `docs/project_plan.md`, `docs/architecture.md`, `docs/protocol/absdk-0630.md` — align product and transport documentation with the approved scope.
|
|
|
|
---
|
|
|
|
### Task 1: Remove the Compatible Provider from the Domain and Persisted Model
|
|
|
|
**Files:**
|
|
- Modify: `domain/src/main/kotlin/com/flagship/abox/manager/domain/Models.kt:26-30`
|
|
- Modify: `android/core/session/src/main/kotlin/com/flagship/abox/manager/session/profile/ProviderProfileStoreImpl.kt:79-92`
|
|
- Create: `android/core/session/src/test/kotlin/com/flagship/abox/manager/session/RemovedProviderProfileTest.kt`
|
|
|
|
**Interfaces:**
|
|
- Consumes: existing `ProviderProfileStore` and `ProviderProfileStoreImpl` APIs.
|
|
- Produces: `ProviderType` with only `ABOX` and `LOCAL_EXPERIENCE`; stale serialized provider names are ignored and cannot become active.
|
|
|
|
- [ ] **Step 1: Write the failing stale-profile persistence test**
|
|
|
|
```kotlin
|
|
package com.flagship.abox.manager.session
|
|
|
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
|
import androidx.datastore.preferences.core.edit
|
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
|
import com.flagship.abox.manager.session.profile.ProviderProfileStoreImpl
|
|
import java.io.File
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.test.runTest
|
|
import org.junit.Assert.assertNull
|
|
import org.junit.Assert.assertTrue
|
|
import org.junit.Test
|
|
|
|
class RemovedProviderProfileTest {
|
|
@Test
|
|
fun removedCompatibleProfileIsIgnoredAndCannotRemainActive() = runTest {
|
|
val file = File.createTempFile("removed-provider", ".preferences_pb").apply { delete() }
|
|
val dataStore = PreferenceDataStoreFactory.create(scope = backgroundScope) { file }
|
|
dataStore.edit { values ->
|
|
values[stringSetPreferencesKey("profile_ids")] = setOf("compatible-default")
|
|
values[stringPreferencesKey("profile_compatible-default_type")] = "COMPATIBLE_BACKEND"
|
|
values[stringPreferencesKey("profile_compatible-default_name")] = "Compatible backend"
|
|
values[stringPreferencesKey("active_profile_id")] = "compatible-default"
|
|
}
|
|
val store = ProviderProfileStoreImpl(dataStore)
|
|
|
|
assertTrue(store.profiles().isEmpty())
|
|
assertNull(store.activeProfile())
|
|
assertNull(store.observeActiveProfile().first())
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run the focused test and verify the baseline behavior**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:core:session:testDebugUnitTest --tests "*RemovedProviderProfileTest"
|
|
```
|
|
|
|
Expected before the enum removal: FAIL because the compatible profile is still parsed and becomes active.
|
|
|
|
- [ ] **Step 3: Remove `COMPATIBLE_BACKEND` and preserve tolerant parsing**
|
|
|
|
Change `ProviderType` to:
|
|
|
|
```kotlin
|
|
enum class ProviderType {
|
|
ABOX,
|
|
LOCAL_EXPERIENCE,
|
|
}
|
|
```
|
|
|
|
Keep the existing tolerant parser:
|
|
|
|
```kotlin
|
|
val type = preferences[profileTypeKey(id)]
|
|
?.let { runCatching { ProviderType.valueOf(it) }.getOrNull() }
|
|
?: return@mapNotNull null
|
|
```
|
|
|
|
Do not map the removed value to ABox or local experience.
|
|
|
|
- [ ] **Step 4: Run domain and session tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :domain:test :android:core:session:testDebugUnitTest
|
|
```
|
|
|
|
Expected: PASS; stale compatible data is ignored, current ABox persistence tests remain green.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```powershell
|
|
git add domain/src/main/kotlin/com/flagship/abox/manager/domain/Models.kt android/core/session/src/test/kotlin/com/flagship/abox/manager/session/RemovedProviderProfileTest.kt
|
|
git commit -m "refactor: remove compatible provider type"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Add the Production Simulation Runtime Module
|
|
|
|
**Files:**
|
|
- Modify: `settings.gradle.kts:25-30`
|
|
- Create: `runtime/simulation/build.gradle.kts`
|
|
- Create: `runtime/simulation/src/main/kotlin/com/flagship/abox/manager/simulation/SimulationSessionGateway.kt`
|
|
- Create: `runtime/simulation/src/main/kotlin/com/flagship/abox/manager/simulation/SimulationDeviceCatalog.kt`
|
|
- Create: `runtime/simulation/src/main/kotlin/com/flagship/abox/manager/simulation/SimulationDeviceGateways.kt`
|
|
- Create: `runtime/simulation/src/test/kotlin/com/flagship/abox/manager/simulation/SimulationSessionGatewayTest.kt`
|
|
- Create: `runtime/simulation/src/test/kotlin/com/flagship/abox/manager/simulation/SimulationDeviceGatewaysTest.kt`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `LocalSessionGateway`, `DeviceCatalog`, `SocketGateway`, `TemperatureHumidityGateway`, `InfraredGateway` from `:domain`.
|
|
- Produces: `SimulationSessionGateway`, `SimulationDeviceCatalog`, `SimulationSocketGateway`, `SimulationTemperatureHumidityGateway`, and `SimulationInfraredGateway` with public no-argument constructors.
|
|
|
|
- [ ] **Step 1: Add the module skeleton and failing session test**
|
|
|
|
Add to `settings.gradle.kts`:
|
|
|
|
```kotlin
|
|
include(":runtime:simulation")
|
|
```
|
|
|
|
Create `runtime/simulation/build.gradle.kts`:
|
|
|
|
```kotlin
|
|
plugins {
|
|
alias(libs.plugins.kotlin.jvm)
|
|
}
|
|
|
|
kotlin {
|
|
jvmToolchain(17)
|
|
}
|
|
|
|
dependencies {
|
|
api(project(":domain"))
|
|
implementation(libs.kotlinx.coroutines.core)
|
|
testImplementation(libs.junit)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
}
|
|
```
|
|
|
|
Create the test:
|
|
|
|
```kotlin
|
|
package com.flagship.abox.manager.simulation
|
|
|
|
import com.flagship.abox.manager.domain.DomainResult
|
|
import com.flagship.abox.manager.domain.LoginCredentials
|
|
import com.flagship.abox.manager.domain.Password
|
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
|
import com.flagship.abox.manager.domain.SessionValidity
|
|
import com.flagship.abox.manager.domain.Username
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.test.runTest
|
|
import org.junit.Assert.assertEquals
|
|
import org.junit.Assert.assertNull
|
|
import org.junit.Test
|
|
|
|
class SimulationSessionGatewayTest {
|
|
@Test
|
|
fun loginObserveValidateAndLogoutAreProfileScoped() = runTest {
|
|
val gateway = SimulationSessionGateway()
|
|
val first = ProviderProfileId("local-one")
|
|
val second = ProviderProfileId("local-two")
|
|
|
|
val result = gateway.login(first, LoginCredentials(Username("local"), Password("")))
|
|
|
|
assertEquals(Username("local"), (result as DomainResult.Success).value.username)
|
|
assertEquals(first, gateway.observeSession(first).first()?.profileId)
|
|
assertNull(gateway.observeSession(second).first())
|
|
assertEquals(DomainResult.Success(SessionValidity.VALID), gateway.validateSession(first))
|
|
gateway.logout(first)
|
|
assertEquals(DomainResult.Success(SessionValidity.INVALID), gateway.validateSession(first))
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run the session test and verify it fails to compile**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :runtime:simulation:test --tests "*SimulationSessionGatewayTest"
|
|
```
|
|
|
|
Expected: FAIL because `SimulationSessionGateway` does not exist.
|
|
|
|
- [ ] **Step 3: Implement `SimulationSessionGateway`**
|
|
|
|
```kotlin
|
|
package com.flagship.abox.manager.simulation
|
|
|
|
import com.flagship.abox.manager.domain.DomainResult
|
|
import com.flagship.abox.manager.domain.LocalSessionGateway
|
|
import com.flagship.abox.manager.domain.LoginCredentials
|
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
|
import com.flagship.abox.manager.domain.Session
|
|
import com.flagship.abox.manager.domain.SessionValidity
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
import kotlinx.coroutines.flow.Flow
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
class SimulationSessionGateway : LocalSessionGateway {
|
|
private val sessions = ConcurrentHashMap<ProviderProfileId, MutableStateFlow<Session?>>()
|
|
|
|
override fun observeSession(profileId: ProviderProfileId): Flow<Session?> = flow(profileId)
|
|
|
|
override suspend fun login(
|
|
profileId: ProviderProfileId,
|
|
credentials: LoginCredentials,
|
|
): DomainResult<Session> = DomainResult.Success(
|
|
Session(profileId, credentials.username).also { flow(profileId).value = it },
|
|
)
|
|
|
|
override suspend fun validateSession(
|
|
profileId: ProviderProfileId,
|
|
): DomainResult<SessionValidity> = DomainResult.Success(
|
|
if (flow(profileId).value == null) SessionValidity.INVALID else SessionValidity.VALID,
|
|
)
|
|
|
|
override suspend fun logout(profileId: ProviderProfileId): DomainResult<Unit> {
|
|
flow(profileId).value = null
|
|
return DomainResult.Success(Unit)
|
|
}
|
|
|
|
private fun flow(profileId: ProviderProfileId): MutableStateFlow<Session?> =
|
|
sessions.getOrPut(profileId) { MutableStateFlow(null) }
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Add failing deterministic device behavior tests**
|
|
|
|
Create tests that assert:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun catalogProvidesSocketTemperatureHumidityAndInfraredDevicesForEachProfile() = runTest {
|
|
val profile = ProviderProfileId("local")
|
|
val devices = (SimulationDeviceCatalog().observeDevices(profile).first() as DomainResult.Success).value
|
|
assertEquals(setOf(DeviceType.SOCKET, DeviceType.TEMPERATURE_HUMIDITY, DeviceType.INFRARED), devices.map { it.type }.toSet())
|
|
assertTrue(devices.all { it.profileId == profile })
|
|
}
|
|
|
|
@Test
|
|
fun socketStateIsMutableAndIsolatedByProfile() = runTest {
|
|
val gateway = SimulationSocketGateway()
|
|
val device = DeviceName("socket-1")
|
|
val first = ProviderProfileId("first")
|
|
val second = ProviderProfileId("second")
|
|
gateway.setPower(first, device, SocketPowerState.ON)
|
|
assertEquals(SocketPowerState.ON, (gateway.getStatus(first, device) as DomainResult.Success).value.powerState)
|
|
assertEquals(SocketPowerState.OFF, (gateway.getStatus(second, device) as DomainResult.Success).value.powerState)
|
|
}
|
|
|
|
@Test
|
|
fun sensorAndInfraredOperationsSucceedWithoutNetworkConfiguration() = runTest {
|
|
val profile = ProviderProfileId("local")
|
|
val th = SimulationTemperatureHumidityGateway().getReading(profile, DeviceName("th-1"))
|
|
assertTrue(th is DomainResult.Success)
|
|
assertEquals(DomainResult.Success(Unit), SimulationInfraredGateway().sendKey(profile, DeviceName("ir-1"), "power"))
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Run device tests and verify they fail to compile**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :runtime:simulation:test --tests "*SimulationDeviceGatewaysTest"
|
|
```
|
|
|
|
Expected: FAIL because the device simulation classes do not exist.
|
|
|
|
- [ ] **Step 6: Implement the deterministic production simulations**
|
|
|
|
Use the same deterministic defaults as the current test fakes, but expose only gateway interfaces. `SimulationInfraredGateway` returns success without public call-recording fields. `SimulationDeviceCatalog` creates IDs prefixed by the supplied profile ID.
|
|
|
|
Core signatures:
|
|
|
|
```kotlin
|
|
class SimulationDeviceCatalog : DeviceCatalog
|
|
class SimulationSocketGateway : SocketGateway
|
|
class SimulationTemperatureHumidityGateway : TemperatureHumidityGateway
|
|
class SimulationInfraredGateway : InfraredGateway
|
|
```
|
|
|
|
Required default values:
|
|
|
|
```text
|
|
socket-1 -> OFF
|
|
th-1 -> temperature "23.5", humidity "45"
|
|
ir-1 -> send/learn/download success
|
|
```
|
|
|
|
- [ ] **Step 7: Run all simulation tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :runtime:simulation:test
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 8: Commit**
|
|
|
|
```powershell
|
|
git add settings.gradle.kts runtime/simulation
|
|
git commit -m "feat: add production local experience simulation"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Route Session Selection to Real ABSDK and Simulation
|
|
|
|
**Files:**
|
|
- Modify: `android/app/build.gradle.kts:54-83`
|
|
- Modify: `android/app/src/main/kotlin/com/flagship/abox/manager/di/SessionModule.kt:1-67`
|
|
- Delete: `android/app/src/debug/kotlin/com/flagship/abox/manager/di/DebugSessionModule.kt`
|
|
- Delete: `android/app/src/release/kotlin/com/flagship/abox/manager/di/ReleaseSessionModule.kt`
|
|
- Delete: `android/core/session/src/main/kotlin/com/flagship/abox/manager/session/gateway/LocalSessionGatewayImpl.kt`
|
|
- Modify: `testing/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/SessionCoordinatorTest.kt`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `AbsdkBridge.create(): AbsdkBridge`, `SimulationSessionGateway`, and existing `SessionCoordinator` constructor.
|
|
- Produces: one all-build Hilt binding where `SessionGateway` is the process-wide `AbsdkBridge` and `LocalSessionGateway` is `SimulationSessionGateway`.
|
|
|
|
- [ ] **Step 1: Extend the coordinator test to prove local selection never calls ABox or credentials**
|
|
|
|
Add counters to test-local doubles or use existing fakes plus an explicit failing remote gateway. The test must assert:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun localSelectionCreatesSessionWithoutAboxOrCredentialAccess() = runTest {
|
|
val remote = CountingSessionGateway()
|
|
val credentials = CountingCredentialStore()
|
|
val local = FakeLocalSessionGateway()
|
|
val profiles = FakeProviderProfileStore()
|
|
val coordinator = SessionCoordinator(profiles, credentials, remote, local)
|
|
|
|
val result = coordinator.selectProvider(localProfile)
|
|
|
|
assertTrue(result is DomainResult.Success)
|
|
assertEquals(0, remote.loginCalls)
|
|
assertEquals(0, credentials.loadCalls + credentials.saveCalls)
|
|
assertEquals(localProfile.id, coordinator.observeActiveSession().first()?.profileId)
|
|
}
|
|
```
|
|
|
|
Also add:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun aboxAuthenticationFailureDoesNotCreateSession() = runTest {
|
|
val fixture = Fixture()
|
|
fixture.coordinator.selectProvider(remoteProfile)
|
|
fixture.remote.failLogin(remoteProfile.id, DomainError.Authentication())
|
|
|
|
val result = fixture.coordinator.login(remoteProfile.id, credentials, false, false)
|
|
|
|
assertTrue(result is DomainResult.Failure)
|
|
assertNull(fixture.coordinator.observeActiveSession().first())
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run coordinator tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :testing:fakes:test --tests "*SessionCoordinatorTest"
|
|
```
|
|
|
|
Expected: existing routing tests pass; the new counter-based test initially fails if it reveals any credential access during local selection.
|
|
|
|
- [ ] **Step 3: Add runtime dependencies and legacy JAR packaging exclusions**
|
|
|
|
In `android/app/build.gradle.kts`, add:
|
|
|
|
```kotlin
|
|
implementation(project(":absdk-bridge"))
|
|
implementation(project(":runtime:simulation"))
|
|
testImplementation(project(":testing:fakes"))
|
|
```
|
|
|
|
Temporarily keep the existing `debugImplementation(project(":testing:fakes"))` until Task 5 removes the remaining debug device bindings; this keeps every intermediate commit compilable. Keep Compose tooling as `debugImplementation`. Add:
|
|
|
|
```kotlin
|
|
packaging {
|
|
resources {
|
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
excludes += "/META-INF/DEPENDENCIES"
|
|
excludes += "/META-INF/LICENSE*"
|
|
excludes += "/META-INF/NOTICE*"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Replace build-type session bindings with uniform production bindings**
|
|
|
|
In `SessionModule.kt`, bind:
|
|
|
|
```kotlin
|
|
@Provides
|
|
@Singleton
|
|
@OptIn(CleartextAbsdkTransport::class)
|
|
fun provideAbsdkBridge(): AbsdkBridge = AbsdkBridge.create()
|
|
|
|
@Provides
|
|
@Singleton
|
|
fun provideRemoteSessionGateway(
|
|
@OptIn(CleartextAbsdkTransport::class) bridge: AbsdkBridge,
|
|
): SessionGateway = bridge
|
|
|
|
@Provides
|
|
@Singleton
|
|
fun provideLocalSessionGateway(): LocalSessionGateway = SimulationSessionGateway()
|
|
```
|
|
|
|
Delete `DebugSessionModule.kt`, `ReleaseSessionModule.kt`, and `LocalSessionGatewayImpl.kt`. Remove obsolete imports.
|
|
|
|
- [ ] **Step 5: Compile both build types and run coordinator tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:compileDebugKotlin :android:app:compileReleaseKotlin :testing:fakes:test --tests "*SessionCoordinatorTest"
|
|
```
|
|
|
|
Expected: PASS. Hilt reports no duplicate `SessionGateway` binding.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```powershell
|
|
git add android/app/build.gradle.kts android/app/src/main/kotlin/com/flagship/abox/manager/di/SessionModule.kt android/app/src/debug/kotlin/com/flagship/abox/manager/di/DebugSessionModule.kt android/app/src/release/kotlin/com/flagship/abox/manager/di/ReleaseSessionModule.kt android/core/session/src/main/kotlin/com/flagship/abox/manager/session/gateway/LocalSessionGatewayImpl.kt testing/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/SessionCoordinatorTest.kt
|
|
git commit -m "feat: route provider sessions to ABSDK and simulation"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Reduce Provider Selection to ABox and Local Experience
|
|
|
|
**Files:**
|
|
- Modify: `android/app/src/main/kotlin/com/flagship/abox/manager/ui/provider/ProviderAvailability.kt`
|
|
- Modify: `android/app/src/main/kotlin/com/flagship/abox/manager/ui/provider/ProviderSelectionViewModel.kt`
|
|
- Modify: `android/app/src/main/kotlin/com/flagship/abox/manager/ui/provider/ProviderSelectionScreen.kt`
|
|
- Modify: `android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt`
|
|
- Create: `android/app/src/test/kotlin/com/flagship/abox/manager/ui/provider/ProviderSelectionViewModelTest.kt`
|
|
- Modify: `android/app/src/androidTest/kotlin/com/flagship/abox/manager/ui/provider/ProviderSelectionScreenTest.kt`
|
|
- Modify: `android/app/src/main/res/values/strings.xml`
|
|
- Modify: `android/app/src/main/res/values-zh-rCN/strings.xml`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `SessionCoordinator.selectProvider(profile)` and two-value `ProviderType`.
|
|
- Produces: provider selection with exactly two enabled options; ABox continues to login, local experience creates a session and transitions directly through `AppSessionViewModel`.
|
|
|
|
- [ ] **Step 1: Add a Main dispatcher rule and write a failing ViewModel provider-list test**
|
|
|
|
Create `android/app/src/test/kotlin/com/flagship/abox/manager/testing/MainDispatcherRule.kt`:
|
|
|
|
```kotlin
|
|
package com.flagship.abox.manager.testing
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
import kotlinx.coroutines.test.TestDispatcher
|
|
import kotlinx.coroutines.test.resetMain
|
|
import kotlinx.coroutines.test.setMain
|
|
import org.junit.rules.TestWatcher
|
|
import org.junit.runner.Description
|
|
|
|
@OptIn(ExperimentalCoroutinesApi::class)
|
|
class MainDispatcherRule(
|
|
val dispatcher: TestDispatcher = StandardTestDispatcher(),
|
|
) : TestWatcher() {
|
|
override fun starting(description: Description) {
|
|
Dispatchers.setMain(dispatcher)
|
|
}
|
|
|
|
override fun finished(description: Description) {
|
|
Dispatchers.resetMain()
|
|
}
|
|
}
|
|
```
|
|
|
|
Create a test fixture with `FakeProviderProfileStore`, `FakeCredentialStore`, `FakeSessionGateway`, and `FakeLocalSessionGateway`. Add `@get:Rule val mainDispatcherRule = MainDispatcherRule()`. Test:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun exposesExactlyAboxAndLocalExperience() {
|
|
val viewModel = ProviderSelectionViewModel(coordinator, ProviderAvailability())
|
|
|
|
assertEquals(
|
|
listOf(ProviderType.ABOX, ProviderType.LOCAL_EXPERIENCE),
|
|
viewModel.uiState.value.providers.map { it.profile.type },
|
|
)
|
|
assertTrue(viewModel.uiState.value.providers.all { it.availability == Availability.Enabled })
|
|
}
|
|
```
|
|
|
|
And:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun selectingLocalExperienceEstablishesSessionImmediately() = runTest {
|
|
val viewModel = ProviderSelectionViewModel(coordinator, ProviderAvailability())
|
|
val local = viewModel.uiState.value.providers.single { it.profile.type == ProviderType.LOCAL_EXPERIENCE }
|
|
|
|
viewModel.select(local)
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(local.profile.id, coordinator.observeActiveSession().first()?.profileId)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run the ViewModel test and verify failure**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:testDebugUnitTest --tests "*ProviderSelectionViewModelTest"
|
|
```
|
|
|
|
Expected: FAIL because current state contains three providers and the availability constructor requires compatible-provider values.
|
|
|
|
- [ ] **Step 3: Simplify availability and provider profiles**
|
|
|
|
Use:
|
|
|
|
```kotlin
|
|
data class ProviderAvailability(
|
|
val abox: Availability = Availability.Enabled,
|
|
val local: Availability = Availability.Enabled,
|
|
) {
|
|
fun forType(type: ProviderType): Availability = when (type) {
|
|
ProviderType.ABOX -> abox
|
|
ProviderType.LOCAL_EXPERIENCE -> local
|
|
}
|
|
}
|
|
|
|
sealed interface Availability {
|
|
data object Enabled : Availability
|
|
data class Disabled(val reason: String) : Availability
|
|
}
|
|
```
|
|
|
|
If no supported provider is disabled, prefer deleting `Disabled` entirely and reduce `ProviderOption` to `ProviderProfile`. Keep it only if the current UI error-state contract still requires it.
|
|
|
|
Set profiles to:
|
|
|
|
```kotlin
|
|
private val PROFILES = listOf(
|
|
ProviderProfile(ProviderProfileId("abox-default"), ProviderType.ABOX, "ABox"),
|
|
ProviderProfile(
|
|
ProviderProfileId("local-experience"),
|
|
ProviderType.LOCAL_EXPERIENCE,
|
|
"Local experience",
|
|
),
|
|
)
|
|
```
|
|
|
|
- [ ] **Step 4: Remove compatible-provider UI branches and update copy**
|
|
|
|
English:
|
|
|
|
```xml
|
|
<string name="provider_abox_description">Connect to the ABox smart-home gateway through ABSDK on the current local network.</string>
|
|
<string name="provider_local_description">Explore the app with simulated local devices and no network connection.</string>
|
|
```
|
|
|
|
Simplified Chinese:
|
|
|
|
```xml
|
|
<string name="provider_abox_description">通过 ABSDK 连接当前局域网中的 ABox 智能家居网关。</string>
|
|
<string name="provider_local_description">无需网络,通过本地模拟设备体验应用。</string>
|
|
```
|
|
|
|
Delete all `provider_compatible_*`, protocol-pending, transport-pending, and simulation-status resources that are no longer referenced.
|
|
|
|
- [ ] **Step 5: Replace the Compose test with the two-option contract**
|
|
|
|
```kotlin
|
|
@Test
|
|
fun aboxAndLocalExperienceAreBothSelectableAndNoCompatibleOptionIsShown() {
|
|
val abox = ProviderOption(
|
|
ProviderProfile(ProviderProfileId("abox"), ProviderType.ABOX, "ABox"),
|
|
Availability.Enabled,
|
|
)
|
|
val local = ProviderOption(
|
|
ProviderProfile(ProviderProfileId("local"), ProviderType.LOCAL_EXPERIENCE, "Local"),
|
|
Availability.Enabled,
|
|
)
|
|
var selected: ProviderOption? = null
|
|
composeRule.setContent {
|
|
ABoxTheme {
|
|
ProviderSelectionScreen(ProviderSelectionUiState(listOf(abox, local))) {
|
|
selected = it
|
|
}
|
|
}
|
|
}
|
|
|
|
composeRule.onNodeWithText("ABox").assertIsEnabled()
|
|
composeRule.onNodeWithText("Local experience").assertIsEnabled().performClick()
|
|
assertEquals(local, selected)
|
|
composeRule.onNodeWithText("Compatible backend").assertDoesNotExist()
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 6: Run provider unit and instrumentation compilation/tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:testDebugUnitTest --tests "*ProviderSelectionViewModelTest" :android:app:compileDebugAndroidTestKotlin
|
|
```
|
|
|
|
If an emulator/device is available, also run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:connectedDebugAndroidTest
|
|
```
|
|
|
|
Expected: unit tests pass; instrumentation source compiles; connected test passes when a device is available.
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```powershell
|
|
git add android/app/src/main/kotlin/com/flagship/abox/manager/ui/provider android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt android/app/src/main/res android/app/src/test android/app/src/androidTest
|
|
git commit -m "refactor: offer ABox and local experience providers"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Add Provider-Aware Device Gateway Routing
|
|
|
|
**Files:**
|
|
- Create: `android/app/src/main/kotlin/com/flagship/abox/manager/di/ProviderRoutingGateways.kt`
|
|
- Create: `android/app/src/test/kotlin/com/flagship/abox/manager/di/ProviderRoutingGatewaysTest.kt`
|
|
- Modify: `android/app/src/debug/kotlin/com/flagship/abox/manager/di/FoundationModule.kt`
|
|
- Create or modify: `android/app/src/main/kotlin/com/flagship/abox/manager/di/DeviceGatewayModule.kt`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `ProviderProfileStore.activeProfile()`, real `AbsdkBridge`, simulation gateway implementations.
|
|
- Produces: `RoutingSocketGateway`, `RoutingTemperatureHumidityGateway`, and `RoutingInfraredGateway`, each implementing the public domain gateway interface.
|
|
|
|
- [ ] **Step 1: Write failing routing tests**
|
|
|
|
Define simple recording test doubles and test both provider types:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun socketRoutesAboxToRealAndLocalToSimulation() = runTest {
|
|
val store = FakeProviderProfileStore()
|
|
val real = RecordingSocketGateway("real")
|
|
val simulation = RecordingSocketGateway("simulation")
|
|
val router = RoutingSocketGateway(store, real, simulation)
|
|
|
|
activate(store, ProviderType.ABOX)
|
|
router.getStatus(activeId, DeviceName("socket-1"))
|
|
assertEquals(1, real.calls)
|
|
assertEquals(0, simulation.calls)
|
|
|
|
activate(store, ProviderType.LOCAL_EXPERIENCE)
|
|
router.getStatus(activeId, DeviceName("socket-1"))
|
|
assertEquals(1, simulation.calls)
|
|
}
|
|
```
|
|
|
|
Add equivalent tests for temperature/humidity and infrared, plus:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun mismatchedOrMissingActiveProfileReturnsInvalidRequest() = runTest {
|
|
val result = router.getStatus(ProviderProfileId("other"), DeviceName("socket-1"))
|
|
assertTrue(result is DomainResult.Failure)
|
|
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run the routing tests and verify failure**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:testDebugUnitTest --tests "*ProviderRoutingGatewaysTest"
|
|
```
|
|
|
|
Expected: FAIL because routing adapters do not exist.
|
|
|
|
- [ ] **Step 3: Implement focused routing adapters**
|
|
|
|
Use a shared resolver:
|
|
|
|
```kotlin
|
|
internal class ActiveProviderGatewayResolver(
|
|
private val profileStore: ProviderProfileStore,
|
|
) {
|
|
suspend fun resolve(profileId: ProviderProfileId): DomainResult<ProviderType> {
|
|
val active = profileStore.activeProfile()
|
|
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "No active provider"))
|
|
if (active.id != profileId) {
|
|
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Provider profile is not active"))
|
|
}
|
|
return DomainResult.Success(active.type)
|
|
}
|
|
}
|
|
```
|
|
|
|
Each router delegates only after a successful resolution. Rethrow `CancellationException`; map unexpected store exceptions to `DomainError.Unknown`.
|
|
|
|
- [ ] **Step 4: Bind public device gateways to routers**
|
|
|
|
Create an all-build Hilt module that receives distinct concrete types:
|
|
|
|
```kotlin
|
|
@Provides
|
|
@Singleton
|
|
fun provideSocketGateway(
|
|
profileStore: ProviderProfileStore,
|
|
real: AbsdkBridge,
|
|
simulation: SimulationSocketGateway,
|
|
): SocketGateway = RoutingSocketGateway(profileStore, real, simulation)
|
|
```
|
|
|
|
Repeat for temperature/humidity and infrared. Provide simulation concrete instances as singletons. Remove the corresponding fake bindings from debug `FoundationModule`.
|
|
|
|
Keep the existing debug-only `FakeDeviceCatalog` binding for now because ABSDK has no catalog and the provider-scoped production ABox catalog belongs to the device-catalog module owner. Do not route ABox catalog requests to `SimulationDeviceCatalog`. Document this temporary boundary in `FoundationModule`. Because `FakeDeviceCatalog` remains debug-only, keep `debugImplementation(project(":testing:fakes"))`; release compilation must not request a `DeviceCatalog` binding until a release consumer actually exists. If Hilt requires one during release compilation, add an explicit release `UnavailableDeviceCatalog` returning `DomainError.InvalidRequest("ABox device catalog is not implemented yet")`, not the simulation catalog.
|
|
|
|
- [ ] **Step 5: Run routing and Hilt compilation tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:testDebugUnitTest --tests "*ProviderRoutingGatewaysTest" :android:app:compileDebugKotlin :android:app:compileReleaseKotlin
|
|
```
|
|
|
|
Expected: PASS; no duplicate gateway bindings.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```powershell
|
|
git add android/app/src/main/kotlin/com/flagship/abox/manager/di android/app/src/debug/kotlin/com/flagship/abox/manager/di/FoundationModule.kt android/app/src/test/kotlin/com/flagship/abox/manager/di
|
|
git commit -m "feat: route device operations by active provider"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Configure Android LAN Transport and Verify Real Login Gating
|
|
|
|
**Files:**
|
|
- Modify: `android/app/src/main/AndroidManifest.xml`
|
|
- Modify: `android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt` only if an additional regression assertion reveals a mismatch.
|
|
- Create: `android/app/src/test/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModelTest.kt`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `AppSessionViewModel.stateFor` behavior via public `uiState`, real `SessionGateway` Hilt binding, Android manifest merge.
|
|
- Produces: authenticated UI only for a session matching the active ABox profile; manifest permits vendor LAN HTTP in every build type.
|
|
|
|
- [ ] **Step 1: Add a regression test for failed ABox login and mismatched sessions**
|
|
|
|
Use `MainDispatcherRule` from Task 4 plus `FakeProviderProfileStore`, `FakeCredentialStore`, `FakeSessionGateway`, `FakeLocalSessionGateway`, and a test `ProviderAvailability`. Assert:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun failedAboxLoginLeavesUiOnLogin() = runTest {
|
|
profiles.saveProfile(abox)
|
|
profiles.setActiveProfile(abox.id)
|
|
remote.failLogin(abox.id, DomainError.Authentication())
|
|
val viewModel = AppSessionViewModel(coordinator, profiles, ProviderAvailability())
|
|
|
|
coordinator.login(abox.id, invalidCredentials, false, false)
|
|
advanceUntilIdle()
|
|
|
|
assertTrue(viewModel.uiState.value is AppSessionUiState.Login)
|
|
}
|
|
|
|
@Test
|
|
fun sessionForDifferentProfileCannotAuthenticateActiveProfile() = runTest {
|
|
// Activate ABox profile A while the gateway exposes a session for profile B.
|
|
// Assert uiState is Login, not Authenticated.
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run the regression test**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:testDebugUnitTest --tests "*AppSessionViewModelTest"
|
|
```
|
|
|
|
Expected: PASS for the current profile-ID guard; if the failed-login case exposes asynchronous setup issues, keep the test and make the smallest state-flow correction.
|
|
|
|
- [ ] **Step 3: Add required application transport declarations**
|
|
|
|
Update the main manifest:
|
|
|
|
```xml
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
<application
|
|
android:name=".ABoxManagerApplication"
|
|
android:allowBackup="false"
|
|
android:label="@string/app_name"
|
|
android:supportsRtl="true"
|
|
android:usesCleartextTraffic="true"
|
|
android:theme="@style/Theme.ABoxManager">
|
|
...
|
|
</application>
|
|
</manifest>
|
|
```
|
|
|
|
Do not add credentials or gateway IP addresses to the manifest.
|
|
|
|
- [ ] **Step 4: Verify manifest merge and both build variants**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:processDebugMainManifest :android:app:processReleaseMainManifest :android:app:testDebugUnitTest --tests "*AppSessionViewModelTest"
|
|
```
|
|
|
|
Expected: PASS; merged manifests contain `INTERNET` and `usesCleartextTraffic=true`.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```powershell
|
|
git add android/app/src/main/AndroidManifest.xml android/app/src/test/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModelTest.kt android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt
|
|
git commit -m "fix: require real ABSDK success before authentication"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Align Product, Architecture, and Protocol Documentation
|
|
|
|
**Files:**
|
|
- Modify: `docs/requirement_spec.md`
|
|
- Modify: `docs/project_plan.md`
|
|
- Modify: `docs/architecture.md`
|
|
- Modify: `docs/protocol/absdk-0630.md`
|
|
|
|
**Interfaces:**
|
|
- Consumes: completed code behavior from Tasks 1-6.
|
|
- Produces: repository documentation that no longer describes cloud or compatible-backend product modes.
|
|
|
|
- [ ] **Step 1: Update the requirement specification**
|
|
|
|
State explicitly:
|
|
|
|
```markdown
|
|
**连接方式**
|
|
1. ABox:通过 ABSDK 连接当前局域网中的智能家居网关,登录成功后进入应用。
|
|
2. 本地体验:不访问网络或 ABSDK,直接建立模拟会话并进入应用。
|
|
3. 首版不提供自有兼容后端入口。
|
|
```
|
|
|
|
Retain remember-username and automatic-login requirements only for ABox.
|
|
|
|
- [ ] **Step 2: Update the project plan**
|
|
|
|
Replace the three-provider scope with two providers, remove `feat/compatible-server`, Ktor/Docker compatibility-server deliverables, and compatible-backend responsibility statements. Preserve the fixed five-person ownership model.
|
|
|
|
- [ ] **Step 3: Update architecture documentation**
|
|
|
|
Document:
|
|
|
|
```text
|
|
ABOX -> AbsdkBridge singleton
|
|
LOCAL_EXPERIENCE -> :runtime:simulation
|
|
```
|
|
|
|
State that `testing:fakes` remains test-only and that ABSDK supplies no device catalog.
|
|
|
|
- [ ] **Step 4: Update protocol documentation with observed LAN evidence**
|
|
|
|
Record without overclaiming:
|
|
|
|
- `192.168.100.1` was reachable on ports 80 and 443 on 2026-08-24;
|
|
- `POST http://192.168.100.1/abox/index.php/abjhsdk/user/validate` returned `code=00000`, `status=0` without a Token;
|
|
- hardware testing on `feat/moler-infrared-control` established that the vendor JAR reaches the real gateway in the current network environment;
|
|
- the JAR still hardcodes `a-box.com.cn`; the mechanism mapping it to the LAN gateway remains an environmental dependency and is not exposed by `setHostInfo`.
|
|
|
|
- [ ] **Step 5: Search for obsolete product claims**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
git grep -n -E "COMPATIBLE_BACKEND|Compatible backend|自有兼容后端|feat/compatible-server|Ktor|PostgreSQL|Docker" -- docs domain android
|
|
```
|
|
|
|
Expected: only explicit historical/removed-value migration notes remain. Any product-scope references are removed.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```powershell
|
|
git add docs/requirement_spec.md docs/project_plan.md docs/architecture.md docs/protocol/absdk-0630.md
|
|
git commit -m "docs: align product scope with LAN ABSDK access"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Full Verification and Authorized Hardware Smoke Test
|
|
|
|
**Files:**
|
|
- Modify only if verification reveals a specific defect.
|
|
- Update: `docs/protocol/absdk-0630.md` with the final authorized hardware result, excluding secrets.
|
|
|
|
**Interfaces:**
|
|
- Consumes: all deliverables from Tasks 1-7.
|
|
- Produces: build evidence and a documented real-vs-local acceptance result.
|
|
|
|
- [ ] **Step 1: Run the full repository verification**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew check testDebugUnitTest test
|
|
```
|
|
|
|
Expected: `BUILD SUCCESSFUL`; all domain, Bridge, simulation, session, app unit, and lint tasks pass.
|
|
|
|
- [ ] **Step 2: Build both APK variants**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
.\gradlew :android:app:assembleDebug :android:app:assembleRelease
|
|
```
|
|
|
|
Expected: both APKs build successfully with the same two provider choices.
|
|
|
|
- [ ] **Step 3: Run the authorized ABox hardware login smoke test**
|
|
|
|
On a device connected to the gateway LAN:
|
|
|
|
1. Install the debug APK.
|
|
2. Choose **ABox**.
|
|
3. Enter a deliberately invalid non-secret credential pair.
|
|
4. Verify the app remains on the login screen and displays the localized authentication error.
|
|
5. Enter an authorized valid credential pair without recording it.
|
|
6. Verify the app enters the authenticated screen only after ABSDK success.
|
|
7. Log out and verify the app returns to ABox login without exposing the password.
|
|
|
|
Expected: invalid credentials cannot create a session; valid credentials can establish the real Bridge session.
|
|
|
|
- [ ] **Step 4: Run the local-experience smoke test**
|
|
|
|
1. Switch provider.
|
|
2. Choose **Local experience**.
|
|
3. Verify no credential page appears.
|
|
4. Verify the authenticated screen opens immediately.
|
|
5. Verify simulated socket, temperature/humidity, and infrared operations work without network access.
|
|
|
|
Expected: local experience is independent from the ABSDK session and credentials.
|
|
|
|
- [ ] **Step 5: Record the sanitized result**
|
|
|
|
Add a dated table row to `docs/protocol/absdk-0630.md` containing:
|
|
|
|
```text
|
|
App commit | build type | provider | invalid-login result | valid-login result | read-only device result | limitations
|
|
```
|
|
|
|
Do not record credentials, Token, API keys, or raw payloads.
|
|
|
|
- [ ] **Step 6: Verify the final diff and working tree**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
git diff --check
|
|
git status --short
|
|
git log --oneline main..HEAD
|
|
```
|
|
|
|
Expected: no whitespace errors; only intentional uncommitted hardware-result documentation remains before the final commit.
|
|
|
|
- [ ] **Step 7: Commit the hardware validation record**
|
|
|
|
```powershell
|
|
git add docs/protocol/absdk-0630.md
|
|
git commit -m "test: record LAN ABSDK login validation"
|
|
```
|
|
|
|
- [ ] **Step 8: Push the branch and update PR #4**
|
|
|
|
```powershell
|
|
git push origin feat/provider-login
|
|
```
|
|
|
|
Use `tea` to update the PR description so it states the new two-provider scope and removes cloud/compatible-backend claims. Do not merge; wait for review and required checks.
|