fix(infrared): support re-learning existing keys, cancel active learning jobs, and propagate CancellationException
This commit is contained in:
+5
@@ -28,6 +28,7 @@ class RoomInfraredKeyRepository(
|
||||
DomainResult.Success(entities.map { it.toDomain() })
|
||||
}
|
||||
.catch { throwable ->
|
||||
if (throwable is kotlinx.coroutines.CancellationException) throw throwable
|
||||
emit(DomainResult.Failure(mapThrowableToDomainError(throwable)))
|
||||
}
|
||||
}
|
||||
@@ -38,6 +39,7 @@ class RoomInfraredKeyRepository(
|
||||
DomainResult.Success(entity?.deviceName?.let { DeviceName(it) })
|
||||
}
|
||||
.catch { throwable ->
|
||||
if (throwable is kotlinx.coroutines.CancellationException) throw throwable
|
||||
emit(DomainResult.Failure(mapThrowableToDomainError(throwable)))
|
||||
}
|
||||
}
|
||||
@@ -182,6 +184,9 @@ class RoomInfraredKeyRepository(
|
||||
}
|
||||
|
||||
private fun mapThrowableToDomainError(throwable: Throwable): DomainError {
|
||||
if (throwable is kotlinx.coroutines.CancellationException) {
|
||||
throw throwable
|
||||
}
|
||||
return when (throwable) {
|
||||
is SQLiteConstraintException -> DomainError.InvalidRequest(detail = "Database constraint violation")
|
||||
else -> DomainError.Unknown(detail = "Database operation failed")
|
||||
|
||||
+27
-8
@@ -19,6 +19,7 @@ import com.flagship.abox.manager.infrared.runtime.InfraredRuntimeModeStore
|
||||
import com.flagship.abox.manager.infrared.runtime.InfraredRuntimeProfileResolver
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -68,6 +69,7 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
private val sendingKeySlotFlow = MutableStateFlow<String?>(null)
|
||||
private val activeLearningSlotFlow = MutableStateFlow<String?>(null)
|
||||
private val messageFlow = MutableStateFlow<RemoteUiMessage?>(null)
|
||||
private var activeLearningJob: Job? = null
|
||||
|
||||
private val effectiveProfileAndMode: Flow<Pair<ProviderProfileId?, InfraredRuntimeMode>> =
|
||||
runtimeModeStore.mode.flatMapLatest { mode ->
|
||||
@@ -149,6 +151,8 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
fun toggleLearningMode(enabled: Boolean) {
|
||||
isLearningModeFlow.value = enabled
|
||||
if (!enabled) {
|
||||
activeLearningJob?.cancel()
|
||||
activeLearningJob = null
|
||||
activeLearningSlotFlow.value = null
|
||||
} else {
|
||||
messageFlow.value = RemoteUiMessage.Info("🔴 学习录制模式已开启:点击任意按键即可开始录制")
|
||||
@@ -168,15 +172,24 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
activeLearningSlotFlow.value = spec.defaultSlot
|
||||
val job = learningCoordinator.learn(
|
||||
profileId = currentProfile,
|
||||
deviceName = savedDevice,
|
||||
displayName = spec.displayName,
|
||||
sdkKey = 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,
|
||||
)
|
||||
}
|
||||
activeLearningJob = job
|
||||
job.invokeOnCompletion {
|
||||
if (activeLearningSlotFlow.value == spec.defaultSlot) {
|
||||
activeLearningSlotFlow.value = null
|
||||
if (activeLearningJob === job) {
|
||||
activeLearningJob = null
|
||||
if (activeLearningSlotFlow.value == spec.defaultSlot) {
|
||||
activeLearningSlotFlow.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
messageFlow.value = RemoteUiMessage.Info("正在录制【${spec.displayName}】(Slot ${spec.defaultSlot}),请按遥控器按键...")
|
||||
@@ -207,6 +220,12 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
activeLearningJob?.cancel()
|
||||
activeLearningJob = null
|
||||
}
|
||||
|
||||
fun clearMessage() {
|
||||
messageFlow.value = null
|
||||
}
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ class DataStoreInfraredRuntimeModeStore(
|
||||
}
|
||||
DomainResult.Success(Unit)
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is kotlinx.coroutines.CancellationException) throw throwable
|
||||
DomainResult.Failure(DomainError.Unknown(detail = "Failed to update runtime mode"))
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -183,4 +183,37 @@ class InfraredRemoteViewModelTest {
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun learningExistingKeyRetriesExistingKey() = testScope.runTest {
|
||||
deviceRepo.setDeviceName(fakeProfile, device)
|
||||
val failedKey = SavedInfraredKey(
|
||||
id = InfraredKeyId("k1"),
|
||||
profileId = fakeProfile,
|
||||
deviceName = device,
|
||||
displayName = "空调-电源",
|
||||
sdkKey = "1",
|
||||
status = InfraredKeyStatus.FAILED,
|
||||
lastErrorCode = "20206",
|
||||
lastErrorDetail = "offline",
|
||||
createdAtEpochMillis = 100L,
|
||||
updatedAtEpochMillis = 100L,
|
||||
)
|
||||
keyRepo.insert(failedKey)
|
||||
|
||||
val vm = createViewModel()
|
||||
val job = launch { vm.state.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
vm.toggleLearningMode(true)
|
||||
vm.onButtonClick(RemoteSceneConfig.AirConditioner.POWER)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, gateway.learnedKeys.size)
|
||||
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)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user