Implemented SocketUiState and SocketViewModel with support for querying power state, toggle control, duplicate click debouncing, and state confirmation timestamping.
Implemented Material 3 responsive SocketScreen with power indicator badge, toggle action, explicit on/off buttons, offline banner, and error handling.
Temperature & Humidity Module:
Implemented TemperatureHumidityUiState and TemperatureHumidityViewModel for read-only environmental telemetry.
Implemented TemperatureHumidityScreen featuring distinct temperature and humidity cards, read-only notice, no-data placeholder, and pull-to-refresh actions.
Common & Infrastructure:
Added UiText and DomainErrorMapper for localizing domain errors (DomainError).
Added navigation graph extensions (deviceDetailGraph) in DeviceDetailNavigation.kt and wired into ABoxManagerApp.kt.
Added ReleaseModule for Hilt dependency injection in release builds.
Added full English and Simplified Chinese resource translations in values/strings.xml and values-zh-rCN/strings.xml.
Testing:
Added SocketViewModelTest and TemperatureHumidityViewModelTest covering normal operations, offline states, error mapping, and refresh scenarios.
Test Plan
Run ./gradlew test (all modules passed)
Run ./gradlew :android:app:testDebugUnitTest (passed)
## Summary
- **Smart Socket Module**:
- Implemented `SocketUiState` and `SocketViewModel` with support for querying power state, toggle control, duplicate click debouncing, and state confirmation timestamping.
- Implemented Material 3 responsive `SocketScreen` with power indicator badge, toggle action, explicit on/off buttons, offline banner, and error handling.
- **Temperature & Humidity Module**:
- Implemented `TemperatureHumidityUiState` and `TemperatureHumidityViewModel` for read-only environmental telemetry.
- Implemented `TemperatureHumidityScreen` featuring distinct temperature and humidity cards, read-only notice, no-data placeholder, and pull-to-refresh actions.
- **Common & Infrastructure**:
- Added `UiText` and `DomainErrorMapper` for localizing domain errors (`DomainError`).
- Added navigation graph extensions (`deviceDetailGraph`) in `DeviceDetailNavigation.kt` and wired into `ABoxManagerApp.kt`.
- Added `ReleaseModule` for Hilt dependency injection in release builds.
- Added full English and Simplified Chinese resource translations in `values/strings.xml` and `values-zh-rCN/strings.xml`.
- **Testing**:
- Added `SocketViewModelTest` and `TemperatureHumidityViewModelTest` covering normal operations, offline states, error mapping, and refresh scenarios.
## Test Plan
- [x] Run `./gradlew test` (all modules passed)
- [x] Run `./gradlew :android:app:testDebugUnitTest` (passed)
- [x] Run `./gradlew assembleDebug` (build successful)
?? Generated with [Claude Code](https://claude.com/claude-code)
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>⏱️ <strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>Security concerns</strong><br><br>
明文传输:release 依赖注入显式启用了 `CleartextAbsdkTransport`,可能使会话信息、设备控制命令及遥测数据遭到中间人窃听或篡改。正式构建应使用经过认证的 TLS 传输。</td></tr>
</table>
Here are some key observations to aid the review process:
⏱️Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪PR contains tests
🔒No security concerns identified
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>⏱️ <strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>No security concerns identified</strong></td></tr>
</table>
Here are some key observations to aid the review process:
⏱️Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪PR contains tests
🔒No security concerns identified
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>⏱️ <strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>No security concerns identified</strong></td></tr>
</table>
Here are some key observations to aid the review process:
⏱️Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪PR contains tests
🔒No security concerns identified
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>⏱️ <strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>No security concerns identified</strong></td></tr>
</table>
Here are some key observations to aid the review process:
⏱️Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪PR contains tests
🔒No security concerns identified
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>⏱️ <strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>No security concerns identified</strong></td></tr>
</table>
Here are some key observations to aid the review process:
⏱️Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪PR contains tests
🔒No security concerns identified
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>⏱️ <strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>No security concerns identified</strong></td></tr>
</table>
Implement ViewModel state management, Compose UI screens, navigation routing,
localized strings, and comprehensive unit tests for smart socket control and
temperature/humidity telemetry monitoring.
- connect DeviceListScreen callbacks to smart socket and temperature/humidity detail routes
- set default smart socket device name to 'Zigbee插座控制器'
- add in-screen dialog to dynamically edit and switch gateway device names
- encode and decode route parameters for non-ASCII device names
- add unit test coverage for dynamic device name switching
Here are some key observations to aid the review process:
⏱️Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪PR contains tests
🔒No security concerns identified
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>⏱️ <strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>No security concerns identified</strong></td></tr>
</table>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
SocketUiStateandSocketViewModelwith support for querying power state, toggle control, duplicate click debouncing, and state confirmation timestamping.SocketScreenwith power indicator badge, toggle action, explicit on/off buttons, offline banner, and error handling.TemperatureHumidityUiStateandTemperatureHumidityViewModelfor read-only environmental telemetry.TemperatureHumidityScreenfeaturing distinct temperature and humidity cards, read-only notice, no-data placeholder, and pull-to-refresh actions.UiTextandDomainErrorMapperfor localizing domain errors (DomainError).deviceDetailGraph) inDeviceDetailNavigation.ktand wired intoABoxManagerApp.kt.ReleaseModulefor Hilt dependency injection in release builds.values/strings.xmlandvalues-zh-rCN/strings.xml.SocketViewModelTestandTemperatureHumidityViewModelTestcovering normal operations, offline states, error mapping, and refresh scenarios.Test Plan
./gradlew test(all modules passed)./gradlew :android:app:testDebugUnitTest(passed)./gradlew assembleDebug(build successful)?? Generated with Claude Code
Inline comment
@@ -14,5 +14,5 @@@Module@InstallIn(SingletonComponent::class)object ReleaseModule {@OptIn(CleartextAbsdkTransport::class)明文传输
正式构建显式放行
CleartextAbsdkTransport,并通过它创建所有网关共用的AbsdkBridge。这会使登录会话、设备命令和遥测数据在不可信网络中面临被窃听或篡改的风险。请为 release 使用 TLS 和服务端身份验证;若 SDK 暂不支持安全传输,应阻止该实现进入正式包。Inline comment
@@ -13,5 +13,5 @@object DeviceDetailRoutes {const val SOCKET_PATTERN = "device/socket/{profileId}/{deviceName}"const val TH_PATTERN = "device/th/{profileId}/{deviceName}"fun socketRoute(profileId: ProviderProfileId, deviceName: DeviceName): String =路由编码
profileId和deviceName被原样拼接到路由路径中。设备名称或配置 ID 包含/、?、#等字符时,会被解析成额外路径段、查询参数或片段,导致目标页面无法匹配或收到截断参数。请对每个路径参数进行 URI 编码,或改用类型安全导航参数。Inline comment
@@ -76,5 +76,5 @@}}}private fun loadStatus() {并发竞态
loadStatus()启动前没有检查已有加载或控制操作。用户切换电源后立即点击刷新,或在离线横幅上连续重试时,可并发执行状态查询与控制请求;较晚返回的旧查询结果可能覆盖刚确认的电源状态,并提前清除加载标志。请在 ViewModel 中串行化或取消冲突请求,而不是仅依赖界面重组后的按钮禁用状态。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
明文传输:release 依赖注入显式启用了
CleartextAbsdkTransport,可能使会话信息、设备控制命令及遥测数据遭到中间人窃听或篡改。正式构建应使用经过认证的 TLS 传输。de3a7d3626to4ea633a329Inline comment
@@ -80,5 +80,5 @@onLogout = sessionActionsViewModel::logout,actionInProgress = actionState.inProgress,errorMessage = actionState.errorMessage,devicesSlot = {DeviceListScreen(设备错配
两个回调都构造固定的中文
DeviceName,而不是传递被点击设备的真实标识。只要提供商中的设备不恰好使用这两个名称,详情页就会用错误的设备键查询或控制,导致DeviceNotFound或操作非预期设备;应让列表项回传实际的DeviceName。Inline comment
@@ -13,5 +13,5 @@object DeviceDetailRoutes {const val SOCKET_PATTERN = "device/socket/{profileId}/{deviceName}"const val TH_PATTERN = "device/th/{profileId}/{deviceName}"fun socketRoute(profileId: ProviderProfileId, deviceName: DeviceName): String =路由编码
profileId.value和deviceName.value被直接拼接到路由路径。设备名包含/、?、#或%时,这些字符会被解释为路由语法,导致目标无法匹配或SavedStateHandle收到错误参数;应对每个路径段进行 URI 编码,或改用类型安全路由。Inline comment
@@ -56,5 +56,5 @@SocketUiEvent.Refresh -> {loadStatus()}SocketUiEvent.TogglePower -> {状态误判
TogglePower将除ON外的状态(包括状态查询失败后的null)一律视为OFF。初次查询发生非离线错误时操作仍会被启用,用户点击切换将直接发送ON;若插座实际已开启,就不会完成预期的关闭操作。应仅在最新电源状态已确认时启用切换,或使用网关提供的原子切换操作。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
4ea633a329tof2102096f0Inline comment
@@ -15,5 +15,5 @@object DeviceDetailRoutes {const val SOCKET_PATTERN = "device/socket/{profileId}/{deviceName}"const val TH_PATTERN = "device/th/{profileId}/{deviceName}"fun socketRoute(profileId: ProviderProfileId, deviceName: DeviceName): String =参数损坏
路由使用
URLEncoder编码,而 Navigation 已会解码路径参数,两个 ViewModel 随后又通过URLDecoder解码一次。设备名或配置 ID 包含字面量+、%2F等内容时会被二次转换,例如A+B会变成A B,导致查询错误设备或找不到设备。应改用适合 URI 路径的编码方式,并移除 ViewModel 中的二次解码。Inline comment
@@ -80,5 +80,5 @@onLogout = sessionActionsViewModel::logout,actionInProgress = actionState.inProgress,errorMessage = actionState.errorMessage,devicesSlot = {DeviceListScreen(目标错误
两个入口始终将固定中文
DeviceName传给网关,而不是用户所选或配置的真实设备名。网关使用英文或自定义名称时,首次状态查询会请求不存在的设备;非中文环境中的详情页标题也会固定显示中文。应从设备列表或配置中传递实际设备名称。Inline comment
@@ -99,5 +99,5 @@}}}private fun loadStatus() {并发竞态
loadStatus()在已有加载进行中时仍会启动新协程,同时离线横幅的重试按钮在加载期间保持可点击。连续重试会产生多个并发查询;如果较新的请求先成功、较旧的请求后失败,旧结果会把界面重新覆盖为离线或错误状态,并放大网络请求。应取消前一个任务或在 ViewModel 中拒绝重复加载;温湿度读取存在相同模式。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
f2102096f0to9872d74dfcInline comment
@@ -21,5 +21,5 @@fun temperatureHumidityRoute(profileId: ProviderProfileId, deviceName: DeviceName): String ="device/th/${encode(profileId.value)}/${encode(deviceName.value)}"private fun encode(value: String): String =参数损坏
URLEncoder按表单规则编码路径段,而 Navigation 会先对路由参数进行 URI 解码,两个 ViewModel 随后又使用URLDecoder再次解码。设备名或配置 ID 包含字面量+、%2F等内容时会被改成空格或其他字符,导致查询错误设备。请改用 URI 路径编码,并确保参数只解码一次。Inline comment
@@ -99,5 +99,5 @@}}}private fun loadStatus() {请求竞态
loadStatus()未阻止加载期间再次启动请求,而离线横幅的重试按钮在isLoading时仍可点击。连续点击会创建多个并发getStatus请求,较早请求若更晚返回,可能覆盖较新的成功状态。应禁用重试,或在 ViewModel 中取消、复用当前加载任务。Inline comment
@@ -86,5 +86,5 @@}}}private fun loadReading(isRefresh: Boolean) {请求竞态
loadReading()没有检查已有加载或刷新任务;离线和无数据卡片中的刷新按钮在isRefreshing时仍保持可用。重复点击会并发查询,同一设备的旧响应可能在新响应之后写回并恢复过期数据或错误状态。应增加单请求保护或取消前一个任务。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
9872d74dfcto8612229e93Inline comment
@@ -79,5 +79,5 @@SocketUiEvent.Refresh -> {loadStatus()}SocketUiEvent.TogglePower -> {切换误操作
当状态查询因网络、认证等非离线错误失败时,
powerState仍为null,但操作按钮保持可用;这里会把未知状态当作关闭并发送开启命令。若插座实际已开启,用户点击切换不会将其关闭。应在状态未知时禁用切换或先重新确认状态。Inline comment
@@ -15,5 +15,5 @@object DeviceDetailRoutes {const val SOCKET_PATTERN = "device/socket/{profileId}/{deviceName}"const val TH_PATTERN = "device/th/{profileId}/{deviceName}"fun socketRoute(profileId: ProviderProfileId, deviceName: DeviceName): String =参数损坏
Navigation 在写入
SavedStateHandle前已经解码路径参数,而两个 ViewModel 又使用URLDecoder再次解码。设备名或配置 ID 包含字面量+、%2F等内容时会被转换成空格或斜杠,导致网关查询错误的设备。应使用适合路径的编码方式,并确保参数只解码一次。Inline comment
@@ -90,5 +90,5 @@private fun loadReading(isRefresh: Boolean) {val profileId = _state.value.profileId ?: returnval deviceName = _state.value.deviceName ?: returnviewModelScope.launch {竞态覆盖
每次刷新都会启动新的协程,且没有检查、取消或标记已有请求;离线和无数据卡片中的重试按钮在刷新期间仍可点击,因此连续点击会产生并发查询。较早的请求若更晚返回,会覆盖较新的温湿度数据或离线状态。应阻止重复请求、取消前一个任务或使用请求序号丢弃过期结果。
PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
8612229e93to525b97c7c5Inline comment
@@ -21,5 +21,5 @@fun temperatureHumidityRoute(profileId: ProviderProfileId, deviceName: DeviceName): String ="device/th/${encode(profileId.value)}/${encode(deviceName.value)}"private fun encode(value: String): String =重复解码
NavController会先对路径参数做 URI 解码,再将其写入SavedStateHandle,但两个详情 ViewModel 又使用URLDecoder解码一次。设备名包含字面量+时,%2B会先还原为+,随后被转换为空格,导致网关使用错误名称查询。请使用适合路径段的编码方式(如Uri.encode),并在 ViewModel 中直接读取已解码的参数。Inline comment
@@ -99,5 +99,5 @@}}}private fun loadStatus() {并发刷新
loadStatus()未阻止已有请求期间再次启动查询,而离线横幅的重试按钮在isLoading时仍可点击,因此连续点击会产生多个并发请求。任一请求先完成都会提前清除加载状态,较早请求若后返回还可能覆盖较新的状态或刚完成的开关结果。请取消或复用前一个任务,或在入口拒绝重复刷新。Inline comment
@@ -86,5 +86,5 @@}}}private fun loadReading(isRefresh: Boolean) {并发刷新
loadReading()同样允许重入,而离线和无数据卡片中的刷新按钮在isRefreshing时仍保持可用。连续点击会并发查询,先完成的请求会提前清除刷新标记,后返回的旧读数可能覆盖更新的数据。请对刷新任务做互斥或取消,并让所有重试入口遵守加载状态。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
525b97c7c5todc45e6affcInline comment
@@ -21,5 +21,5 @@fun temperatureHumidityRoute(profileId: ProviderProfileId, deviceName: DeviceName): String ="device/th/${encode(profileId.value)}/${encode(deviceName.value)}"private fun encode(value: String): String =参数损坏
这里使用表单语义的
URLEncoder,而两个 ViewModel 又对 Navigation 已解码的参数调用URLDecoder,会发生二次解码。设备名或配置 ID 含字面量+(如plug+1)时会被改成空格,含%xx的名称也可能被再次改写,最终查询错误设备或提示设备不存在。请使用适合 URI 路径段的编码方式,并确保参数只解码一次。Inline comment
@@ -14,5 +14,5 @@val isOffline: Boolean = false,val errorMessage: UiText? = null,val lastConfirmedTimeMillis: Long? = null,) {val isActionEnabled: Boolean状态误判
isActionEnabled未要求powerState已知。首次状态查询发生非离线错误后,操作按钮仍会启用,而isPowerOn会把未知状态当作关闭;此时点击主按钮会直接发送开启命令,即使插座实际已经开启,也无法执行用户预期的切换。应在状态未知时禁用切换操作,或为显式开关命令和切换命令设置不同的启用条件。Inline comment
@@ -86,5 +86,5 @@}}}private fun loadReading(isRefresh: Boolean) {请求竞态
每次刷新都会直接启动新的协程,既未拒绝已有加载,也未取消旧请求;同时无数据和离线卡片中的刷新按钮在刷新期间仍保持可点击。快速重复点击会并发执行多个
getReading,较早请求若较晚返回,就会覆盖较新的遥测结果并提前清除加载状态。建议保存并取消前一个任务,或在入口原子地阻止重复请求;SocketViewModel的状态加载也存在相同模式。PR Reviewer Guide 🔍
Here are some key observations to aid the review process: