fix(infrared): add relearn use case for ready keys and clean up interrupted learning in onCleared and init
This commit is contained in:
+16
@@ -5,15 +5,21 @@ 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.InfraredKeyRepository
|
||||
import com.flagship.abox.manager.domain.infrared.LearnInfraredKey
|
||||
import com.flagship.abox.manager.domain.infrared.RelearnInfraredKey
|
||||
import com.flagship.abox.manager.domain.infrared.RetryInfraredKeyLearning
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class InfraredLearningCoordinator(
|
||||
private val learnInfraredKey: LearnInfraredKey,
|
||||
private val retryInfraredKeyLearning: RetryInfraredKeyLearning,
|
||||
private val relearnInfraredKey: RelearnInfraredKey,
|
||||
private val keyRepository: InfraredKeyRepository,
|
||||
private val externalScope: CoroutineScope,
|
||||
) {
|
||||
fun learn(
|
||||
@@ -28,4 +34,14 @@ class InfraredLearningCoordinator(
|
||||
fun retry(ref: InfraredKeyRef): Deferred<DomainResult<Unit>> = externalScope.async {
|
||||
retryInfraredKeyLearning(ref)
|
||||
}
|
||||
|
||||
fun relearn(ref: InfraredKeyRef): Deferred<DomainResult<Unit>> = externalScope.async {
|
||||
relearnInfraredKey(ref)
|
||||
}
|
||||
|
||||
fun failInterruptedLearning(profileId: ProviderProfileId, updatedAtEpochMillis: Long = System.currentTimeMillis()) {
|
||||
externalScope.launch(NonCancellable) {
|
||||
keyRepository.failInterruptedLearning(profileId, updatedAtEpochMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -170,15 +170,28 @@ object InfraredFeatureModule {
|
||||
clock: EpochMillisClock,
|
||||
): RecoverInterruptedInfraredLearning = RecoverInterruptedInfraredLearning(repository, clock)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRelearnInfraredKey(
|
||||
repository: InfraredKeyRepository,
|
||||
gateway: InfraredGateway,
|
||||
clock: EpochMillisClock,
|
||||
): com.flagship.abox.manager.domain.infrared.RelearnInfraredKey =
|
||||
com.flagship.abox.manager.domain.infrared.RelearnInfraredKey(repository, gateway, clock)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideInfraredLearningCoordinator(
|
||||
learnInfraredKey: LearnInfraredKey,
|
||||
retryInfraredKeyLearning: RetryInfraredKeyLearning,
|
||||
relearnInfraredKey: com.flagship.abox.manager.domain.infrared.RelearnInfraredKey,
|
||||
repository: InfraredKeyRepository,
|
||||
@InfraredApplicationScope scope: CoroutineScope,
|
||||
): InfraredLearningCoordinator = InfraredLearningCoordinator(
|
||||
learnInfraredKey = learnInfraredKey,
|
||||
retryInfraredKeyLearning = retryInfraredKeyLearning,
|
||||
relearnInfraredKey = relearnInfraredKey,
|
||||
keyRepository = repository,
|
||||
externalScope = scope,
|
||||
)
|
||||
}
|
||||
|
||||
+14
-7
@@ -74,6 +74,12 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
private var activeLearningJob: Job? = null
|
||||
private var underlyingLearningDeferred: kotlinx.coroutines.Deferred<*>? = null
|
||||
|
||||
init {
|
||||
authenticatedProfileId?.let { profile ->
|
||||
learningCoordinator.failInterruptedLearning(profile)
|
||||
}
|
||||
}
|
||||
|
||||
private data class InfraredRemoteConfigSnapshot(
|
||||
val effectiveProfileId: ProviderProfileId? = null,
|
||||
val runtimeMode: InfraredRuntimeMode = InfraredRuntimeMode.FAKE,
|
||||
@@ -151,9 +157,7 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
activeLearningJob = null
|
||||
activeLearningSlotFlow.value = null
|
||||
state.value.effectiveProfileId?.let { profile ->
|
||||
viewModelScope.launch(kotlinx.coroutines.NonCancellable) {
|
||||
keyRepository.failInterruptedLearning(profile, System.currentTimeMillis())
|
||||
}
|
||||
learningCoordinator.failInterruptedLearning(profile)
|
||||
}
|
||||
} else {
|
||||
messageFlow.value = RemoteUiMessage.Info("🔴 学习录制模式已开启:点击任意按键即可开始录制")
|
||||
@@ -178,10 +182,10 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
}
|
||||
messageFlow.value = RemoteUiMessage.Info("正在录制【${spec.displayName}】(Slot ${spec.defaultSlot}),请按遥控器按键...")
|
||||
activeLearningSlotFlow.value = spec.defaultSlot
|
||||
val deferred = if (existingKey != null) {
|
||||
learningCoordinator.retry(InfraredKeyRef(currentProfile, existingKey.id))
|
||||
} else {
|
||||
learningCoordinator.learn(
|
||||
val deferred = when (existingKey?.status) {
|
||||
InfraredKeyStatus.FAILED -> learningCoordinator.retry(InfraredKeyRef(currentProfile, existingKey.id))
|
||||
InfraredKeyStatus.READY -> learningCoordinator.relearn(InfraredKeyRef(currentProfile, existingKey.id))
|
||||
else -> learningCoordinator.learn(
|
||||
profileId = currentProfile,
|
||||
deviceName = savedDevice,
|
||||
displayName = spec.displayName,
|
||||
@@ -254,6 +258,9 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
underlyingLearningDeferred = null
|
||||
activeLearningJob?.cancel()
|
||||
activeLearningJob = null
|
||||
state.value.effectiveProfileId?.let { profile ->
|
||||
learningCoordinator.failInterruptedLearning(profile)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMessage() {
|
||||
|
||||
+54
-2
@@ -9,6 +9,7 @@ import com.flagship.abox.manager.domain.infrared.InfraredKeyId
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyRef
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyStatus
|
||||
import com.flagship.abox.manager.domain.infrared.LearnInfraredKey
|
||||
import com.flagship.abox.manager.domain.infrared.RelearnInfraredKey
|
||||
import com.flagship.abox.manager.domain.infrared.RetryInfraredKeyLearning
|
||||
import com.flagship.abox.manager.domain.infrared.SavedInfraredKey
|
||||
import com.flagship.abox.manager.testing.fakes.FakeInfraredGateway
|
||||
@@ -34,10 +35,11 @@ class InfraredLearningCoordinatorTest {
|
||||
|
||||
private val learn = LearnInfraredKey(repository, gateway, factory, clock)
|
||||
private val retry = RetryInfraredKeyLearning(repository, gateway, clock)
|
||||
private val relearn = RelearnInfraredKey(repository, gateway, clock)
|
||||
|
||||
@Test
|
||||
fun learnRunsAsynchronouslyInExternalScope() = runTest {
|
||||
val coordinator = InfraredLearningCoordinator(learn, retry, this)
|
||||
val coordinator = InfraredLearningCoordinator(learn, retry, relearn, repository, this)
|
||||
|
||||
coordinator.learn(profile, device, "Power")
|
||||
advanceUntilIdle()
|
||||
@@ -65,7 +67,7 @@ class InfraredLearningCoordinatorTest {
|
||||
)
|
||||
repository.insert(failedKey)
|
||||
|
||||
val coordinator = InfraredLearningCoordinator(learn, retry, this)
|
||||
val coordinator = InfraredLearningCoordinator(learn, retry, relearn, repository, this)
|
||||
|
||||
coordinator.retry(InfraredKeyRef(profile, InfraredKeyId("key1")))
|
||||
advanceUntilIdle()
|
||||
@@ -73,4 +75,54 @@ class InfraredLearningCoordinatorTest {
|
||||
val updated = (repository.getKey(InfraredKeyRef(profile, InfraredKeyId("key1"))) as DomainResult.Success).value!!
|
||||
assertEquals(InfraredKeyStatus.READY, updated.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relearnRunsAsynchronouslyInExternalScope() = runTest {
|
||||
val readyKey = SavedInfraredKey(
|
||||
id = InfraredKeyId("key1"),
|
||||
profileId = profile,
|
||||
deviceName = device,
|
||||
displayName = "Power",
|
||||
sdkKey = "app_ir_key1",
|
||||
status = InfraredKeyStatus.READY,
|
||||
lastErrorCode = null,
|
||||
lastErrorDetail = null,
|
||||
createdAtEpochMillis = 500L,
|
||||
updatedAtEpochMillis = 500L,
|
||||
)
|
||||
repository.insert(readyKey)
|
||||
|
||||
val coordinator = InfraredLearningCoordinator(learn, retry, relearn, repository, this)
|
||||
|
||||
coordinator.relearn(InfraredKeyRef(profile, InfraredKeyId("key1")))
|
||||
advanceUntilIdle()
|
||||
|
||||
val updated = (repository.getKey(InfraredKeyRef(profile, InfraredKeyId("key1"))) as DomainResult.Success).value!!
|
||||
assertEquals(InfraredKeyStatus.READY, updated.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failInterruptedLearningFailsLearningKeys() = runTest {
|
||||
val learningKey = SavedInfraredKey(
|
||||
id = InfraredKeyId("key1"),
|
||||
profileId = profile,
|
||||
deviceName = device,
|
||||
displayName = "Power",
|
||||
sdkKey = "app_ir_key1",
|
||||
status = InfraredKeyStatus.LEARNING,
|
||||
lastErrorCode = null,
|
||||
lastErrorDetail = null,
|
||||
createdAtEpochMillis = 500L,
|
||||
updatedAtEpochMillis = 500L,
|
||||
)
|
||||
repository.insert(learningKey)
|
||||
|
||||
val coordinator = InfraredLearningCoordinator(learn, retry, relearn, repository, this)
|
||||
|
||||
coordinator.failInterruptedLearning(profile, 2_000L)
|
||||
advanceUntilIdle()
|
||||
|
||||
val updated = (repository.getKey(InfraredKeyRef(profile, InfraredKeyId("key1"))) as DomainResult.Success).value!!
|
||||
assertEquals(InfraredKeyStatus.FAILED, updated.status)
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -64,13 +64,14 @@ class InfraredRemoteViewModelTest {
|
||||
|
||||
private val learn = LearnInfraredKey(keyRepo, gateway, factory, clock)
|
||||
private val retry = RetryInfraredKeyLearning(keyRepo, gateway, clock)
|
||||
private val relearn = com.flagship.abox.manager.domain.infrared.RelearnInfraredKey(keyRepo, gateway, clock)
|
||||
private val send = SendInfraredKey(keyRepo, gateway)
|
||||
private lateinit var coordinator: InfraredLearningCoordinator
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
coordinator = InfraredLearningCoordinator(learn, retry, testScope)
|
||||
coordinator = InfraredLearningCoordinator(learn, retry, relearn, keyRepo, testScope)
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -270,4 +271,38 @@ class InfraredRemoteViewModelTest {
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relearningExistingReadyKeyOverwritesKey() = testScope.runTest {
|
||||
deviceRepo.setDeviceName(fakeProfile, device)
|
||||
val readyKey = SavedInfraredKey(
|
||||
id = InfraredKeyId("k1"),
|
||||
profileId = fakeProfile,
|
||||
deviceName = device,
|
||||
displayName = "空调-电源",
|
||||
sdkKey = "1",
|
||||
status = InfraredKeyStatus.READY,
|
||||
lastErrorCode = null,
|
||||
lastErrorDetail = null,
|
||||
createdAtEpochMillis = 100L,
|
||||
updatedAtEpochMillis = 100L,
|
||||
)
|
||||
keyRepo.insert(readyKey)
|
||||
|
||||
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)
|
||||
assertEquals(RemoteUiMessage.Info("【空调-电源】录制成功"), vm.state.value.message)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -71,6 +71,7 @@ class InfraredControlViewModelTest {
|
||||
|
||||
private val learnUseCase = LearnInfraredKey(keyRepository, gateway, factory, clock)
|
||||
private val retryUseCase = RetryInfraredKeyLearning(keyRepository, gateway, clock)
|
||||
private val relearnUseCase = com.flagship.abox.manager.domain.infrared.RelearnInfraredKey(keyRepository, gateway, clock)
|
||||
private val sendUseCase = SendInfraredKey(keyRepository, gateway)
|
||||
private val renameUseCase = RenameInfraredKey(keyRepository, clock)
|
||||
private val deleteUseCase = DeleteInfraredKey(keyRepository)
|
||||
@@ -81,7 +82,7 @@ class InfraredControlViewModelTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
coordinator = InfraredLearningCoordinator(learnUseCase, retryUseCase, testScope)
|
||||
coordinator = InfraredLearningCoordinator(learnUseCase, retryUseCase, relearnUseCase, keyRepository, testScope)
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
Reference in New Issue
Block a user