diff --git a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelViewModel.kt b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelViewModel.kt index 221dad3..6027600 100644 --- a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelViewModel.kt +++ b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelViewModel.kt @@ -24,6 +24,8 @@ import com.flagship.abox.manager.infrared.remote.UiText import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -59,6 +61,7 @@ class RemotePanelViewModel @Inject constructor( private val localAcStateFlow = MutableStateFlow(null) private val messageFlow = MutableStateFlow(null) private var acDebounceJob: Job? = null + private var learningJob: Job? = null private val remoteFlow = if (profileId == null || remoteId == null) { flowOf(null) @@ -128,6 +131,7 @@ class RemotePanelViewModel @Inject constructor( fun toggleLearnMode() { val newMode = !isLearnModeFlow.value isLearnModeFlow.value = newMode + if (!newMode) cancelLearning() if (newMode) { messageFlow.value = PanelUiMessage.Info(UiText.StringResource(R.string.infrared_msg_learning_mode_enabled)) } @@ -167,7 +171,7 @@ class RemotePanelViewModel @Inject constructor( if (state.value.learningButtonId != null) return learningButtonIdFlow.value = buttonId - viewModelScope.launch { + learningJob = viewModelScope.launch { try { when (val res = learnVirtualRemoteKey(currentProfile, currentRemoteId, buttonId)) { is DomainResult.Success -> { @@ -175,12 +179,39 @@ class RemotePanelViewModel @Inject constructor( } is DomainResult.Failure -> handleDomainError(res.error) } + } catch (cancelled: kotlinx.coroutines.CancellationException) { + val binding = state.value.bindings.firstOrNull { it.buttonId == buttonId } + if (binding != null) { + withContext(NonCancellable) { + virtualRemoteRepository.updateKeyBinding( + binding.copy( + status = com.flagship.abox.manager.domain.infrared.InfraredKeyStatus.FAILED, + lastErrorCode = null, + lastErrorDetail = "学习已取消,需要重新学习", + ), + ) + } + } + throw cancelled } finally { learningButtonIdFlow.value = null } } } + fun cancelLearning() { + learningJob?.cancel() + learningJob = null + learningButtonIdFlow.value = null + isLearnModeFlow.value = false + } + + override fun onCleared() { + cancelLearning() + acDebounceJob?.cancel() + super.onCleared() + } + fun addCustomButton(displayName: String) { val currentProfile = profileId ?: return val currentRemoteId = remoteId ?: return diff --git a/android/feature/infrared/src/test/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelViewModelTest.kt b/android/feature/infrared/src/test/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelViewModelTest.kt index 8db53e1..e29023c 100644 --- a/android/feature/infrared/src/test/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelViewModelTest.kt +++ b/android/feature/infrared/src/test/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelViewModelTest.kt @@ -226,4 +226,29 @@ class RemotePanelViewModelTest { job.cancel() } + + @Test + fun cancellingLearningModeClearsActiveLearningState() = testScope.runTest { + val res = createRemote( + profileId = profileId, + deviceName = device, + displayName = "TV", + type = RemoteDeviceType.TV, + brandModel = "generic_tv", + transmissionMode = TransmissionMode.STATIC_SLOTS, + ) + val remoteId = (res as com.flagship.abox.manager.domain.DomainResult.Success).value + val vm = createViewModel(remoteId.value) + val job = launch { vm.state.collect {} } + advanceUntilIdle() + + vm.toggleLearnMode() + runCurrent() + vm.cancelLearning() + runCurrent() + + assertEquals(false, vm.state.value.isLearnMode) + assertEquals(null, vm.state.value.learningButtonId) + job.cancel() + } }