fix(infrared): propagate learning error result and localize remote screen strings
This commit is contained in:
+6
-4
@@ -1,13 +1,15 @@
|
||||
package com.flagship.abox.manager.infrared.application
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyId
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyRef
|
||||
import com.flagship.abox.manager.domain.infrared.LearnInfraredKey
|
||||
import com.flagship.abox.manager.domain.infrared.RetryInfraredKeyLearning
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
class InfraredLearningCoordinator(
|
||||
private val learnInfraredKey: LearnInfraredKey,
|
||||
@@ -19,11 +21,11 @@ class InfraredLearningCoordinator(
|
||||
deviceName: DeviceName,
|
||||
displayName: String,
|
||||
sdkKey: String? = null,
|
||||
): Job = externalScope.launch {
|
||||
): Deferred<DomainResult<InfraredKeyId>> = externalScope.async {
|
||||
learnInfraredKey(profileId, deviceName, displayName, sdkKey)
|
||||
}
|
||||
|
||||
fun retry(ref: InfraredKeyRef): Job = externalScope.launch {
|
||||
fun retry(ref: InfraredKeyRef): Deferred<DomainResult<Unit>> = externalScope.async {
|
||||
retryInfraredKeyLearning(ref)
|
||||
}
|
||||
}
|
||||
|
||||
+30
-14
@@ -171,27 +171,43 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
if (activeLearningSlotFlow.value != null) {
|
||||
return
|
||||
}
|
||||
activeLearningSlotFlow.value = spec.defaultSlot
|
||||
val existingKey = state.value.learnedSlotMap[spec.defaultSlot]
|
||||
val job = if (existingKey != null) {
|
||||
learningCoordinator.retry(InfraredKeyRef(currentProfile, existingKey.id))
|
||||
} else {
|
||||
learningCoordinator.learn(
|
||||
profileId = currentProfile,
|
||||
deviceName = savedDevice,
|
||||
displayName = spec.displayName,
|
||||
sdkKey = spec.defaultSlot,
|
||||
)
|
||||
if (existingKey?.status == InfraredKeyStatus.LEARNING) {
|
||||
return
|
||||
}
|
||||
activeLearningJob = job
|
||||
job.invokeOnCompletion {
|
||||
if (activeLearningJob === job) {
|
||||
activeLearningJob = null
|
||||
activeLearningSlotFlow.value = spec.defaultSlot
|
||||
val job = viewModelScope.launch {
|
||||
try {
|
||||
val result = if (existingKey != null) {
|
||||
learningCoordinator.retry(InfraredKeyRef(currentProfile, existingKey.id)).await()
|
||||
} else {
|
||||
when (val res = learningCoordinator.learn(
|
||||
profileId = currentProfile,
|
||||
deviceName = savedDevice,
|
||||
displayName = spec.displayName,
|
||||
sdkKey = spec.defaultSlot,
|
||||
).await()) {
|
||||
is DomainResult.Success -> DomainResult.Success(Unit)
|
||||
is DomainResult.Failure -> DomainResult.Failure(res.error)
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
is DomainResult.Success -> {
|
||||
messageFlow.value = RemoteUiMessage.Info("【${spec.displayName}】录制成功")
|
||||
}
|
||||
is DomainResult.Failure -> {
|
||||
handleDomainError(result.error)
|
||||
}
|
||||
}
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
throw e
|
||||
} finally {
|
||||
if (activeLearningSlotFlow.value == spec.defaultSlot) {
|
||||
activeLearningSlotFlow.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
activeLearningJob = job
|
||||
messageFlow.value = RemoteUiMessage.Info("正在录制【${spec.displayName}】(Slot ${spec.defaultSlot}),请按遥控器按键...")
|
||||
} else {
|
||||
if (sendingKeySlotFlow.value != null) {
|
||||
|
||||
+12
-6
@@ -37,12 +37,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.flagship.abox.manager.designsystem.ABoxSurface
|
||||
import com.flagship.abox.manager.infrared.R
|
||||
import com.flagship.abox.manager.infrared.remote.InfraredRemoteUiState
|
||||
import com.flagship.abox.manager.infrared.remote.InfraredRemoteViewModel
|
||||
import com.flagship.abox.manager.infrared.remote.RemoteButtonSpec
|
||||
@@ -108,13 +110,13 @@ internal fun InfraredRemoteScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "红外遥控器",
|
||||
text = stringResource(R.string.infrared_remote_title),
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onBack) {
|
||||
Text("返回")
|
||||
Text(stringResource(R.string.infrared_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
@@ -124,7 +126,11 @@ internal fun InfraredRemoteScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = if (state.isLearningMode) "录制中" else "学习模式",
|
||||
text = if (state.isLearningMode) {
|
||||
stringResource(R.string.infrared_remote_recording)
|
||||
} else {
|
||||
stringResource(R.string.infrared_remote_learning_mode)
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
fontWeight = if (state.isLearningMode) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if (state.isLearningMode) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface,
|
||||
@@ -178,7 +184,7 @@ internal fun InfraredRemoteScreen(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "🔴 红外学习模式已开启:点击任意按键即可录制信号",
|
||||
text = stringResource(R.string.infrared_remote_learning_banner),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
@@ -196,7 +202,7 @@ internal fun InfraredRemoteScreen(
|
||||
FilterChip(
|
||||
selected = state.selectedScene == RemoteScene.AIR_CONDITIONER,
|
||||
onClick = { onSceneSelected(RemoteScene.AIR_CONDITIONER) },
|
||||
label = { Text("❄️ 空调遥控") },
|
||||
label = { Text(stringResource(R.string.infrared_remote_scene_ac)) },
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.testTag("scene_tab_ac"),
|
||||
@@ -208,7 +214,7 @@ internal fun InfraredRemoteScreen(
|
||||
FilterChip(
|
||||
selected = state.selectedScene == RemoteScene.TELEVISION,
|
||||
onClick = { onSceneSelected(RemoteScene.TELEVISION) },
|
||||
label = { Text("📺 电视遥控") },
|
||||
label = { Text(stringResource(R.string.infrared_remote_scene_tv)) },
|
||||
modifier = Modifier.testTag("scene_tab_tv"),
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
|
||||
@@ -32,4 +32,11 @@
|
||||
<string name="infrared_dialog_cancel">取消</string>
|
||||
<string name="infrared_dialog_confirm">确认</string>
|
||||
<string name="infrared_back">返回</string>
|
||||
<string name="infrared_remote_title">红外遥控器</string>
|
||||
<string name="infrared_remote_recording">录制中</string>
|
||||
<string name="infrared_remote_learning_mode">学习模式</string>
|
||||
<string name="infrared_remote_learning_banner">🔴 红外学习模式已开启:点击任意按键即可录制信号</string>
|
||||
<string name="infrared_remote_scene_ac">❄️ 空调遥控</string>
|
||||
<string name="infrared_remote_scene_tv">📺 电视遥控</string>
|
||||
<string name="infrared_remote_settings">设置</string>
|
||||
</resources>
|
||||
|
||||
@@ -32,4 +32,11 @@
|
||||
<string name="infrared_dialog_cancel">Cancel</string>
|
||||
<string name="infrared_dialog_confirm">Confirm</string>
|
||||
<string name="infrared_back">Back</string>
|
||||
<string name="infrared_remote_title">Infrared Remote</string>
|
||||
<string name="infrared_remote_recording">Recording</string>
|
||||
<string name="infrared_remote_learning_mode">Learn Mode</string>
|
||||
<string name="infrared_remote_learning_banner">🔴 Learn mode active: tap any button to record</string>
|
||||
<string name="infrared_remote_scene_ac">❄️ Air Conditioner</string>
|
||||
<string name="infrared_remote_scene_tv">📺 Television</string>
|
||||
<string name="infrared_remote_settings">Settings</string>
|
||||
</resources>
|
||||
|
||||
+37
@@ -213,6 +213,43 @@ class InfraredRemoteViewModelTest {
|
||||
assertEquals("1", gateway.learnedKeys.first().key)
|
||||
val updated = (keyRepo.getKey(com.flagship.abox.manager.domain.infrared.InfraredKeyRef(fakeProfile, InfraredKeyId("k1"))) as DomainResult.Success).value!!
|
||||
assertEquals(InfraredKeyStatus.READY, updated.status)
|
||||
assertEquals(RemoteUiMessage.Info("【空调-电源】录制成功"), vm.state.value.message)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun learningFailurePropagatesErrorMessage() = testScope.runTest {
|
||||
deviceRepo.setDeviceName(fakeProfile, device)
|
||||
gateway.learnResult = DomainResult.Failure(com.flagship.abox.manager.domain.DomainError.DeviceOffline(externalCode = "20206"))
|
||||
|
||||
val vm = createViewModel()
|
||||
val job = launch { vm.state.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
vm.toggleLearningMode(true)
|
||||
vm.onButtonClick(RemoteSceneConfig.AirConditioner.POWER)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(RemoteUiMessage.Error("红外控制器离线 (错误码: 20206)"), vm.state.value.message)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun learningSessionInvalidPropagatesSessionInvalidMessage() = testScope.runTest {
|
||||
deviceRepo.setDeviceName(fakeProfile, device)
|
||||
gateway.learnResult = DomainResult.Failure(com.flagship.abox.manager.domain.DomainError.TokenInvalid(detail = "Session expired"))
|
||||
|
||||
val vm = createViewModel()
|
||||
val job = launch { vm.state.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
vm.toggleLearningMode(true)
|
||||
vm.onButtonClick(RemoteSceneConfig.AirConditioner.POWER)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(RemoteUiMessage.SessionInvalid, vm.state.value.message)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user