Adds scene integration phase 1: domain model, fakes, UI, navigation, and tests.
Changes
domain: Add Scene, SceneAction (SocketPower/InfraredKey), SceneRepository, SceneExecutor, and execution result models.
testing/fakes: Add FakeSceneRepository (isolated by ProviderProfileId, seeded defaults for the local profile) and FakeSceneExecutor (sequential execution, continue-on-failure).
android/app: Add scene UI module - list screen, editor screen, execution progress screen - with Hilt-bound ViewModels.
navigation: Wire scene routes into ABoxManagerApp NavHost.
strings: Add scene_* entries for en and zh-rCN.
tests: Cover FakeSceneRepository CRUD/isolation and FakeSceneExecutor ordering/continue-on-failure.
Test plan
./gradlew :testing:fakes:test passes.
FakeSceneRepository isolates scenes by provider profile.
FakeSceneExecutor runs actions in order and continues on failure.
Notes
Phase 1 ships fakes only; real repository/executor backed by the abox gateway will land in a follow-up.
## Summary
Adds scene integration phase 1: domain model, fakes, UI, navigation, and tests.
## Changes
- **domain**: Add `Scene`, `SceneAction` (SocketPower/InfraredKey), `SceneRepository`, `SceneExecutor`, and execution result models.
- **testing/fakes**: Add `FakeSceneRepository` (isolated by `ProviderProfileId`, seeded defaults for the local profile) and `FakeSceneExecutor` (sequential execution, continue-on-failure).
- **android/app**: Add scene UI module - list screen, editor screen, execution progress screen - with Hilt-bound ViewModels.
- **navigation**: Wire scene routes into `ABoxManagerApp` NavHost.
- **strings**: Add `scene_*` entries for en and zh-rCN.
- **tests**: Cover `FakeSceneRepository` CRUD/isolation and `FakeSceneExecutor` ordering/continue-on-failure.
## Test plan
- [x] `./gradlew :testing:fakes:test` passes.
- [x] `FakeSceneRepository` isolates scenes by provider profile.
- [x] `FakeSceneExecutor` runs actions in order and continues on failure.
## Notes
Phase 1 ships fakes only; real repository/executor backed by the abox gateway will land in a follow-up.
Here are some key observations to aid the review process:
⏱️Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪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>: 5 🔵🔵🔵🔵🔵</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: 5 🔵🔵🔵🔵🔵
🧪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>: 5 🔵🔵🔵🔵🔵</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>No security concerns identified</strong></td></tr>
</table>
## 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>
跨配置文件操作:执行页缓存初始化时加载的场景,提供方切换后不会重新校验活动 `ProviderProfile`,可能向非当前活动提供方的设备发送控制命令。应在执行前验证 `scene.profileId`,并在活动配置文件变化时退出或重新加载场景。</td></tr>
</table>
Here are some key observations to aid the review process:
⏱️Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪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>: 5 🔵🔵🔵🔵🔵</td></tr>
<tr><td>🧪 <strong>PR contains tests</strong></td></tr>
<tr><td>🔒 <strong>No security concerns identified</strong></td></tr>
</table>
保存时重新读取当前活动配置,而已加载场景及其动作仍属于打开编辑器时的配置。如果用户在编辑器保留期间切换配置,代码会用新配置 ID 保存包含旧配置动作的场景;仓库的配置一致性校验可能直接抛出异常,或把更新提交到错误的配置。应在初始化时固定场景所属的 profileId,并在配置变化时关闭或重新初始化编辑器。
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
Adds scene integration phase 1: domain model, fakes, UI, navigation, and tests.
Changes
Scene,SceneAction(SocketPower/InfraredKey),SceneRepository,SceneExecutor, and execution result models.FakeSceneRepository(isolated byProviderProfileId, seeded defaults for the local profile) andFakeSceneExecutor(sequential execution, continue-on-failure).ABoxManagerAppNavHost.scene_*entries for en and zh-rCN.FakeSceneRepositoryCRUD/isolation andFakeSceneExecutorordering/continue-on-failure.Test plan
./gradlew :testing:fakes:testpasses.FakeSceneRepositoryisolates scenes by provider profile.FakeSceneExecutorruns actions in order and continues on failure.Notes
Phase 1 ships fakes only; real repository/executor backed by the abox gateway will land in a follow-up.
Inline comment
@@ -189,5 +189,5 @@}}internal fun com.flagship.abox.manager.domain.SceneColor.toColorOrNull(): Color? {val hex = value.removePrefix("#")编译错误
SceneColor只定义了hex属性,此处访问不存在的value会导致主源码无法编译。应改为使用hex.removePrefix("#")。Inline comment
@@ -19,5 +19,5 @@* [FakeSceneExecutor] that drives the existing socket/infrared fakes. The bindings live in the* debug source set so the release build does not ship the Fake implementation; the real provider* bindings will land in `feat/scene-persistence` and `feat/provider-login` respectively.*/@Module发布构建
SceneRepository、SceneExecutor和带LocalProfileId限定符的ProviderProfileId仅在 debug 源集中提供,但依赖它们的 ViewModel 位于 main 源集。当前没有 release 绑定,执行 release 变体的 Hilt 组件生成时会出现 MissingBinding;需要提供 release 实现或将相关消费者限制在 debug 变体。Inline comment
@@ -53,5 +53,5 @@) {val scenes by viewModel.scenes.collectAsStateWithLifecycle()val pendingDelete by viewModel.pendingDelete.collectAsStateWithLifecycle()Scaffold(实验 API
此处使用了 Material 3 的
TopAppBar,但文件没有像另外两个新增页面一样声明ExperimentalMaterial3Apiopt-in。在该 API 仍为实验状态的依赖版本下,这会导致编译失败;应为对应 Composable 添加@OptIn(ExperimentalMaterial3Api::class)。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
c9dc3e7a77to6b4ef585fbInline comment
@@ -56,5 +56,5 @@androidx.compose.runtime.LaunchedEffect(state.isSaved) {if (state.isSaved) onSaved()}androidx.compose.runtime.LaunchedEffect(sceneId) {状态丢失
LaunchedEffect(sceneId)会在 Activity 配置变更后重新执行,但 Hilt ViewModel 会被保留;此时initialize会重置新场景状态或从仓库覆盖编辑状态,导致用户旋转屏幕后丢失尚未保存的名称、颜色和动作。应确保同一sceneId只初始化一次,或通过SavedStateHandle保存编辑状态。Inline comment
@@ -25,5 +25,5 @@override suspend fun createScene(profileId: ProviderProfileId,scene: Scene,): DomainResult<Scene> {require(scene.profileId == profileId) {隔离失效
创建和更新时只校验
Scene.profileId,没有校验每个SceneAction.profileId。传入包含其他配置动作的场景会被保存并发布到当前配置,违反SceneRepository声明的动作隔离约束;配置切换期间保存编辑状态即可触发这种混合数据。请同时校验所有动作均属于传入的配置,并同步修复 fake 实现。Inline comment
@@ -28,5 +28,5 @@): DomainResult<Scene> {require(scene.profileId == profileId) {"Scene profileId must match the supplied profileId"}val scenes = currentScenes(profileId).orEmpty()并发覆盖
创建操作采用非原子的“读取、检查、发布”流程。两个线程同时为同一配置创建不同场景时,可能读取相同旧列表,后一次发布会覆盖前一次结果并静默丢失场景;更新和删除也有相同问题。
ConcurrentHashMap只保护映射本身,应使用按配置的互斥锁或原子状态更新包住完整的读改写操作。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
Inline comment
@@ -56,5 +56,5 @@androidx.compose.runtime.LaunchedEffect(state.isSaved) {if (state.isSaved) onSaved()}androidx.compose.runtime.LaunchedEffect(sceneId) {草稿丢失
配置更改导致界面重建时,此副作用会在保留原 Hilt ViewModel 的情况下再次调用
initialize。该方法会重置新建场景或重新加载已有场景,因此用户尚未保存的名称、颜色和动作会被静默覆盖。应让初始化针对同一sceneId保持幂等,或通过SavedStateHandle只初始化一次。Inline comment
@@ -36,5 +36,5 @@fun initialize(sceneId: SceneId) {_uiState.value = SceneExecutionUiState.LoadingviewModelScope.launch {val profile = profileStore.activeProfile()跨配置执行
活动配置文件仅在初始化时读取,加载后的场景会一直保存在
Ready状态。若用户停留在执行页时切换提供方,随后仍会使用旧场景的profileId向网关发送命令。应监听配置文件切换并使当前场景失效,或在执行前重新校验活动配置文件与scene.profileId一致。Inline comment
@@ -50,5 +50,5 @@}}}fun execute() {重复执行
execute明确接受Running状态,因此快速重复点击或重复事件会启动第二次执行。执行器会拒绝第二次调用,而该失败会把仍在正常运行的界面状态覆盖为Failed。此处应仅允许从Ready状态开始执行。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
跨配置文件操作:执行页缓存初始化时加载的场景,提供方切换后不会重新校验活动
ProviderProfile,可能向非当前活动提供方的设备发送控制命令。应在执行前验证scene.profileId,并在活动配置文件变化时退出或重新加载场景。Inline comment
@@ -44,5 +44,5 @@private val _state = MutableStateFlow(SceneEditorState())val state: StateFlow<SceneEditorState> = _state.asStateFlow()fun initialize(sceneId: SceneId?) {状态重置
initialize对相同参数也会无条件重置或重新加载状态。Activity 配置变更后 ViewModel 会保留,但界面的LaunchedEffect会再次调用此方法,导致新建场景的输入全部丢失,或编辑中的修改被仓库版本覆盖。应记录已初始化的场景 ID,或使用SavedStateHandle保存编辑状态,避免重复初始化。Inline comment
@@ -33,5 +33,5 @@private val _uiState = MutableStateFlow<SceneExecutionUiState>(SceneExecutionUiState.Loading)val uiState: StateFlow<SceneExecutionUiState> = _uiState.asStateFlow()fun initialize(sceneId: SceneId) {执行状态丢失
对同一场景重复调用
initialize会把Running或Completed无条件重置为Loading,随后变为Ready。配置变更会重新触发界面初始化,因此已完成的结果会消失;执行过程中旋转设备还会重新显示执行按钮,而原协程仍在运行。应让初始化对同一场景幂等,并保留进行中及完成状态。Inline comment
@@ -28,5 +28,5 @@): DomainResult<Scene> {require(scene.profileId == profileId) {"Scene profileId must match the supplied profileId"}val scenes = currentScenes(profileId).orEmpty()更新丢失
场景修改采用非原子的“读取列表后再发布”流程。两个协程同时为同一配置创建场景时,可能都基于相同旧列表计算结果,最后一次发布会覆盖前一次,静默丢失一个场景;更新和删除也存在同类竞争。应使用按配置隔离的互斥锁或其他原子事务保护完整的校验与写入过程。
PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
Inline comment
@@ -31,5 +31,5 @@val startTime = System.currentTimeMillis()val results = mutableListOf<SceneActionExecutionResult>()var aborted = falsereturn try {并行失效
执行逻辑完全未读取
scene.concurrencyMode,所有动作始终通过循环串行执行。用户选择PARALLEL后,延时等动作仍会阻塞后续动作,使编辑器中的并行设置失效,并显著改变执行时长和预期行为。Inline comment
@@ -107,5 +107,5 @@)}}is SceneAction.Delay -> {取消失效
catch (e: Exception)会捕获协程的CancellationException并将其转换为普通动作失败。执行在延时期间被取消时,CONTINUE_ON_ERROR场景可能继续触发后续设备动作;应显式重新抛出取消异常。Inline comment
@@ -90,5 +90,5 @@)toExecutionResult(action, res)}is SceneAction.VirtualRemoteButton -> {虚假成功
构造器允许省略
sendVirtualRemoteAction,但此时虚拟遥控动作不会发送任何命令,却仍被记录为成功。任何未注入该依赖的运行环境都会向用户报告执行成功,而实际设备没有响应;应将依赖设为必需或返回明确失败。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
⚠️ Review coverage: The following files were not included in this review because of the token budget:
android/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModelTest.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneEditorViewModelTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/Scene.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepository.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneExecutorTest.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneRepositoryTest.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneNavigation.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModel.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModelTest.ktdomain/src/test/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepositoryTest.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/di/SceneIntegrationModule.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecution.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneRepository.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecutor.ktsettings.gradle.ktsandroid/app/src/main/res/values-zh-rCN/strings.xmlandroid/app/src/main/res/values/strings.xmlgradle.propertiesInline comment
@@ -896,5 +896,5 @@var notifyMsg by remember { mutableStateOf("自动化任务执行完成") }val coroutineScope = rememberCoroutineScope()LaunchedEffect(availableRemotes, selectedRemoteIndex) {绑定竞态
LaunchedEffect内又通过独立的rememberCoroutineScope启动查询,切换遥控器时旧查询不会随 Effect 取消,较慢的旧结果可能覆盖当前遥控器的按键列表。用户快速切换后确认时,可能保存“当前遥控器 + 另一遥控器按键”的错误组合;应直接在LaunchedEffect中执行挂起查询,并在切换时清空旧绑定。Inline comment
@@ -53,5 +53,5 @@val isSaving: Boolean = false,val isSaved: Boolean = false,val errorMessage: String? = null,) {val isValid: Boolean get() = name.isNotBlank() && actions.isNotEmpty()校验缺失
isValid没有要求自动场景至少包含一个触发器,因此用户只填写名称和动作就能保存一个已启用但永远不会被触发的自动场景。应在类型为AUTOMATIC时要求triggers非空,或将其明确保存为禁用草稿。Inline comment
@@ -54,5 +54,5 @@): DomainResult<Scene> {require(scene.profileId == profileId) {"Scene profileId must match the supplied profileId"}val scenes = currentScenes(profileId).orEmpty()并发丢失
创建场景采用非原子的“读取列表后再发布”流程。同一配置下两个并发
createScene都可能基于相同旧列表追加并返回成功,但后发布的结果会覆盖前一个,导致其中一个场景丢失。应对每个配置使用互斥锁,或将重复检查和列表更新合并为原子操作。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
⚠️ Review coverage: The following files were not included in this review because of the token budget:
android/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModelTest.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneEditorViewModelTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/Scene.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepository.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneExecutorTest.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneRepositoryTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutor.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneNavigation.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModel.ktdomain/src/test/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepositoryTest.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/di/SceneIntegrationModule.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecution.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneRepository.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecutor.ktdomain/build.gradle.ktsandroid/app/src/main/res/values-zh-rCN/strings.xmlandroid/app/src/main/res/values/strings.xmlgradle.propertiesInline comment
@@ -289,5 +289,5 @@deviceName = action.deviceName,key = action.key,)is SceneAction.VirtualRemoteButton,虚假成功
VirtualRemoteButton、Delay和Notification都被直接当作成功处理,实际没有发送遥控、等待指定时长或发出通知。用户添加这些动作后,执行页会显示成功但副作用未发生,尤其顺序场景中的延时完全失效。请实现对应行为,或将不支持的动作返回明确失败。Inline comment
@@ -759,5 +759,5 @@FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text("💧 湿度") })}when (selectedType) {0 -> {OutlinedTextField(输入校验
timeInput可以被编辑为任意字符串,确认按钮始终可用并直接创建触发器。输入abc、25:99等值时,可能因领域构造校验异常而崩溃,或保存出无法调度的自动场景。应先按HH:mm解析和校验,无效时阻止提交并展示错误。Inline comment
@@ -324,5 +324,5 @@)}}if (state.actions.isEmpty()) {本地化缺失
新界面仍包含大量用户可见的中文硬编码,例如空动作提示,以及后续对话框标签和描述。在英文 locale 下会出现中英混排,新增的英文字符串资源也无法生效。请将这些文案统一移入
strings.xml和values-zh-rCN/strings.xml。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
⚠️ Review coverage: The following files were not included in this review because of the token budget:
domain/src/test/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutorTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepository.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneRepositoryTest.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModelTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/Scene.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneExecutorTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutor.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModelTest.ktdomain/src/test/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepositoryTest.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneNavigation.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModel.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/di/SceneIntegrationModule.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecution.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneRepository.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecutor.ktsettings.gradle.ktsdomain/build.gradle.ktsandroid/app/src/main/res/values-zh-rCN/strings.xmlandroid/app/src/main/res/values/strings.xmlgradle.propertiesInline comment
@@ -103,5 +103,5 @@)}return}viewModelScope.launch {编辑竞态
编辑现有场景时,
sceneId只有在异步查询成功后才写入状态,且没有加载态阻止编辑或保存。仓库响应较慢或失败时,页面会按“新建”处理;用户若完成有效输入并保存,save()将调用createScene,产生重复场景而不是更新目标。应先绑定待编辑的 ID,并在加载完成前禁用保存。Inline comment
@@ -224,5 +224,5 @@)}val startTime = System.currentTimeMillis()return try {策略失效
PARALLEL分支一次性启动所有动作,完全未处理scene.strategy。当用户组合选择并行与ABORT_ON_ERROR时,即使一个动作失败,其余插座或红外等副作用仍会执行,使“遇错中止”失效。应禁止该组合,或在失败后取消尚未完成的任务并明确已启动动作的语义。Inline comment
@@ -76,5 +76,5 @@}private fun observeAvailableRemotes() {viewModelScope.launch {profileStore.observeActiveProfile().collectLatest { profile ->设备硬编码
observeRemotes将红外设备名固定为中文字符串红外控制器。只要当前配置中的实际设备名不同,例如英文名称、自定义名称或默认设备 ID,仓库查询就无法加载该设备的虚拟遥控器,遥控动作选择器会错误显示为空。应从当前配置或设备列表取得真实的DeviceName。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
⚠️ Review coverage: The following files were not included in this review because of the token budget:
domain/src/test/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutorTest.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneEditorViewModelTest.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneExecutorTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepository.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneRepositoryTest.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModelTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/Scene.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutor.ktdomain/src/test/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepositoryTest.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneNavigation.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModel.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/di/SceneIntegrationModule.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecution.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneRepository.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecutor.ktandroid/app/src/main/res/values-zh-rCN/strings.xmlandroid/app/src/main/res/values/strings.xmlgradle.propertiesInline comment
@@ -229,5 +229,5 @@return try {val results: List<SceneActionExecutionResult> = if (scene.concurrencyMode == ConcurrencyMode.PARALLEL) {if (scene.strategy == ExecutionStrategy.ABORT_ON_ERROR) {coroutineScope {lateinit var deferreds: List<Deferred<SceneActionExecutionResult>>并发崩溃
并行且遇错终止时,子协程启动后会访问尚未完成赋值的
deferreds。在Main.immediate或Unconfined等可立即执行子协程的调度器上,首个动作同步失败会抛出UninitializedPropertyAccessException,导致执行器直接异常退出而不是返回失败结果。应先以惰性启动方式构建完整列表,再统一启动任务。Inline comment
@@ -1104,5 +1104,5 @@}}},confirmButton = {val canConfirm = when (actionType) {无效遥控器
当没有可用虚拟遥控器时,确认按钮仍默认可用,并创建仓库中不存在的硬编码
default-ir遥控器动作。没有配置遥控器的用户因此可以保存一个必然可能在执行时失败的场景;应禁用确认操作,或在确实需要原始红外回退时创建对应的InfraredKey动作。Inline comment
@@ -342,5 +342,5 @@val state = _state.valueif (!state.isValid || state.isSaving) return_state.update { it.copy(isSaving = true, errorMessage = null) }viewModelScope.launch {val profile = profileStore.activeProfile()配置串用
保存时重新读取当前活动配置,而已加载场景及其动作仍属于打开编辑器时的配置。如果用户在编辑器保留期间切换配置,代码会用新配置 ID 保存包含旧配置动作的场景;仓库的配置一致性校验可能直接抛出异常,或把更新提交到错误的配置。应在初始化时固定场景所属的
profileId,并在配置变化时关闭或重新初始化编辑器。PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
⚠️ Review coverage: The following files were not included in this review because of the token budget:
android/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionScreen.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneExecutorTest.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneEditorViewModelTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepository.kttesting/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeSceneRepositoryTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutor.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModelTest.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/Scene.ktandroid/app/src/test/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModelTest.ktdomain/src/test/kotlin/com/flagship/abox/manager/domain/InMemorySceneRepositoryTest.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneNavigation.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneListViewModel.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.ktandroid/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecution.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneRepository.ktdomain/src/main/kotlin/com/flagship/abox/manager/domain/SceneExecutor.ktsettings.gradle.ktsandroid/app/src/main/res/values-zh-rCN/strings.xmlandroid/app/src/main/res/values/strings.xmlgradle.properties