From 0620b9f6c88ca5b42117ba6adae0f39b3d2958ff Mon Sep 17 00:00:00 2001 From: mxhsnd <2049295946@qq.com> Date: Mon, 24 Aug 2026 14:13:47 +0800 Subject: [PATCH] feat: support 1-100 numeric infrared key slots and gateway login entry --- .../abox/manager/di/FoundationModule.kt | 4 +- .../abox/manager/ui/ABoxManagerApp.kt | 151 ++++++++++++++++-- .../abox/manager/ui/FoundationViewModel.kt | 103 ++++++++++++ .../infrared/ui/InfraredControlScreenTest.kt | 12 +- .../InfraredLearningCoordinator.kt | 9 +- .../infrared/ui/InfraredControlScreen.kt | 20 ++- .../infrared/ui/InfraredControlViewModel.kt | 19 ++- .../src/main/res/values-zh-rCN/strings.xml | 2 + .../infrared/src/main/res/values/strings.xml | 2 + .../domain/infrared/InfraredKeyFactory.kt | 19 ++- .../domain/infrared/LearnInfraredKey.kt | 3 +- .../domain/infrared/InfraredKeyFactoryTest.kt | 21 ++- 12 files changed, 329 insertions(+), 36 deletions(-) create mode 100644 android/app/src/main/kotlin/com/flagship/abox/manager/ui/FoundationViewModel.kt diff --git a/android/app/src/debug/kotlin/com/flagship/abox/manager/di/FoundationModule.kt b/android/app/src/debug/kotlin/com/flagship/abox/manager/di/FoundationModule.kt index 1680d07..9b6801a 100644 --- a/android/app/src/debug/kotlin/com/flagship/abox/manager/di/FoundationModule.kt +++ b/android/app/src/debug/kotlin/com/flagship/abox/manager/di/FoundationModule.kt @@ -24,7 +24,9 @@ import javax.inject.Singleton object FoundationModule { @Provides @Singleton - fun provideSessionGateway(): SessionGateway = FakeSessionGateway() + fun provideSessionGateway( + @OptIn(CleartextAbsdkTransport::class) real: AbsdkBridge, + ): SessionGateway = real @Provides @Singleton diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.kt index 5e55d50..5fc8d4e 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.kt @@ -2,9 +2,19 @@ package com.flagship.abox.manager.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -14,8 +24,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -84,34 +98,139 @@ private fun FoundationScreen( sessionInvalidMessage: String?, onContinue: () -> Unit, onOpenInfrared: () -> Unit, + viewModel: FoundationViewModel = hiltViewModel(), ) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + Column( modifier = Modifier .fillMaxSize() - .padding(32.dp), + .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - MessageScreen( - title = stringResource(R.string.foundation_title), - description = sessionInvalidMessage ?: stringResource(R.string.foundation_description), + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.foundation_title), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, ) - ABoxButton( - onClick = onContinue, - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp), + Text( + text = sessionInvalidMessage ?: stringResource(R.string.foundation_description), + style = MaterialTheme.typography.bodyMedium, + color = if (sessionInvalidMessage != null) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + + // 临时网关登录卡片 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + ), ) { - Text(stringResource(R.string.continue_action)) + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "ABox 网关登录 (Real 模式前置)", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + if (state.session != null) { + OutlinedButton( + onClick = viewModel::logout, + enabled = !state.isLoading, + ) { + Text("退出") + } + } + } + + if (state.session == null) { + OutlinedTextField( + value = state.usernameInput, + onValueChange = viewModel::onUsernameChanged, + label = { Text("账号 (默认 a)") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = state.passwordInput, + onValueChange = viewModel::onPasswordChanged, + label = { Text("密码 (默认 a)") }, + visualTransformation = PasswordVisualTransformation(), + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + ABoxButton( + onClick = viewModel::login, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading, + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary, + ) + } else { + Text("连接并登录网关") + } + } + } else { + Text( + text = "✅ 已登录 ABox (账号: ${state.session?.username?.value})", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium, + ) + Text( + text = "网关会话已建立,现在可以在红外控制中使用真实 ABSDK 模式。", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + state.errorMessage?.let { error -> + Text( + text = "⚠️ $error", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + state.successMessage?.let { success -> + Text( + text = "ℹ️ $success", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + ) + } + } } + + Spacer(modifier = Modifier.weight(1f)) + ABoxButton( onClick = onOpenInfrared, - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp), + modifier = Modifier.fillMaxWidth(), ) { Text(stringResource(R.string.infrared_control_action)) } + + OutlinedButton( + onClick = onContinue, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.continue_action)) + } } } @@ -127,12 +246,12 @@ private fun MessageScreen( ) { Text( text = title, - style = androidx.compose.material3.MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.headlineMedium, textAlign = TextAlign.Center, ) Text( text = description, - style = androidx.compose.material3.MaterialTheme.typography.bodyLarge, + style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, ) } diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/FoundationViewModel.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/FoundationViewModel.kt new file mode 100644 index 0000000..bf5b888 --- /dev/null +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/FoundationViewModel.kt @@ -0,0 +1,103 @@ +package com.flagship.abox.manager.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.flagship.abox.manager.domain.DomainError +import com.flagship.abox.manager.domain.DomainResult +import com.flagship.abox.manager.domain.LoginCredentials +import com.flagship.abox.manager.domain.Password +import com.flagship.abox.manager.domain.ProviderProfileId +import com.flagship.abox.manager.domain.Session +import com.flagship.abox.manager.domain.SessionGateway +import com.flagship.abox.manager.domain.Username +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class FoundationUiState( + val usernameInput: String = "a", + val passwordInput: String = "a", + val isLoading: Boolean = false, + val session: Session? = null, + val errorMessage: String? = null, + val successMessage: String? = null, +) + +@HiltViewModel +class FoundationViewModel @Inject constructor( + private val sessionGateway: SessionGateway, +) : ViewModel() { + + private val profileId = ProviderProfileId("abox") + private val _uiState = MutableStateFlow(FoundationUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { + sessionGateway.observeSession(profileId).collect { session -> + _uiState.value = _uiState.value.copy(session = session) + } + } + } + + fun onUsernameChanged(value: String) { + _uiState.value = _uiState.value.copy(usernameInput = value, errorMessage = null) + } + + fun onPasswordChanged(value: String) { + _uiState.value = _uiState.value.copy(passwordInput = value, errorMessage = null) + } + + fun login() { + val username = _uiState.value.usernameInput.trim() + val password = _uiState.value.passwordInput.trim() + if (username.isBlank() || password.isBlank()) { + _uiState.value = _uiState.value.copy(errorMessage = "用户名和密码不能为空") + return + } + + _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null, successMessage = null) + + viewModelScope.launch { + val result = sessionGateway.login( + profileId = profileId, + credentials = LoginCredentials(Username(username), Password(password)), + ) + when (result) { + is DomainResult.Success -> { + _uiState.value = _uiState.value.copy( + isLoading = false, + session = result.value, + successMessage = "登录成功!已获取 ABox 会话 Token", + ) + } + is DomainResult.Failure -> { + val detail = when (val error = result.error) { + is DomainError.Authentication -> "认证失败 (错误码: ${error.externalCode ?: "20004"}),请检查账号密码" + is DomainError.Network -> "网络连接失败: ${error.detail ?: "请确认已连接网关 WiFi"}" + is DomainError.TokenInvalid -> "Token 无效" + else -> error.detail ?: "登录失败 (${error.externalCode ?: "unknown"})" + } + _uiState.value = _uiState.value.copy( + isLoading = false, + errorMessage = detail, + ) + } + } + } + } + + fun logout() { + viewModelScope.launch { + sessionGateway.logout(profileId) + _uiState.value = _uiState.value.copy(session = null, successMessage = "已退出登录") + } + } + + fun clearMessage() { + _uiState.value = _uiState.value.copy(errorMessage = null, successMessage = null) + } +} diff --git a/android/feature/infrared/src/androidTest/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlScreenTest.kt b/android/feature/infrared/src/androidTest/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlScreenTest.kt index 211cefe..e670e3f 100644 --- a/android/feature/infrared/src/androidTest/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlScreenTest.kt +++ b/android/feature/infrared/src/androidTest/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlScreenTest.kt @@ -3,7 +3,6 @@ package com.flagship.abox.manager.infrared.ui import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.flagship.abox.manager.domain.DeviceName @@ -13,7 +12,6 @@ import com.flagship.abox.manager.domain.infrared.InfraredKeyStatus import com.flagship.abox.manager.domain.infrared.SavedInfraredKey import com.flagship.abox.manager.infrared.runtime.InfraredRuntimeMode import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -36,11 +34,12 @@ class InfraredControlScreenTest { composeTestRule.setContent { InfraredControlScreen( state = state, + nextSlot = 1, onBack = {}, onSessionInvalid = {}, onDeviceNameChanged = {}, onSaveDeviceName = {}, - onLearn = {}, + onLearn = { _, _ -> }, onRetry = {}, onSend = {}, onRename = { _, _ -> }, @@ -63,7 +62,7 @@ class InfraredControlScreenTest { profileId = ProviderProfileId("abox"), deviceName = DeviceName("ir-1"), displayName = "Power Key", - sdkKey = "app_ir_kready", + sdkKey = "1", status = InfraredKeyStatus.READY, lastErrorCode = null, lastErrorDetail = null, @@ -75,7 +74,7 @@ class InfraredControlScreenTest { profileId = ProviderProfileId("abox"), deviceName = DeviceName("ir-1"), displayName = "Volume Up", - sdkKey = "app_ir_kfailed", + sdkKey = "2", status = InfraredKeyStatus.FAILED, lastErrorCode = "20206", lastErrorDetail = null, @@ -96,11 +95,12 @@ class InfraredControlScreenTest { composeTestRule.setContent { InfraredControlScreen( state = state, + nextSlot = 3, onBack = {}, onSessionInvalid = {}, onDeviceNameChanged = {}, onSaveDeviceName = {}, - onLearn = {}, + onLearn = { _, _ -> }, onRetry = { retriedKeyId = it }, onSend = { sentKeyId = it }, onRename = { _, _ -> }, diff --git a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/application/InfraredLearningCoordinator.kt b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/application/InfraredLearningCoordinator.kt index 8dbc7ef..a191321 100644 --- a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/application/InfraredLearningCoordinator.kt +++ b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/application/InfraredLearningCoordinator.kt @@ -13,9 +13,14 @@ class InfraredLearningCoordinator( private val retryInfraredKeyLearning: RetryInfraredKeyLearning, private val externalScope: CoroutineScope, ) { - fun learn(profileId: ProviderProfileId, deviceName: DeviceName, displayName: String) { + fun learn( + profileId: ProviderProfileId, + deviceName: DeviceName, + displayName: String, + sdkKey: String? = null, + ) { externalScope.launch { - learnInfraredKey(profileId, deviceName, displayName) + learnInfraredKey(profileId, deviceName, displayName, sdkKey) } } diff --git a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlScreen.kt b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlScreen.kt index 61b94b1..6f575ce 100644 --- a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlScreen.kt +++ b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlScreen.kt @@ -1,6 +1,5 @@ package com.flagship.abox.manager.infrared.ui -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -58,6 +57,7 @@ fun InfraredControlRoute( InfraredControlScreen( state = state, + nextSlot = viewModel.nextAvailableKeySlot(), onBack = onBack, onSessionInvalid = onSessionInvalid, onDeviceNameChanged = viewModel::onDeviceNameChanged, @@ -76,11 +76,12 @@ fun InfraredControlRoute( @Composable internal fun InfraredControlScreen( state: InfraredControlUiState, + nextSlot: Int, onBack: () -> Unit, onSessionInvalid: () -> Unit, onDeviceNameChanged: (String) -> Unit, onSaveDeviceName: () -> Unit, - onLearn: (String) -> Unit, + onLearn: (String, String?) -> Unit, onRetry: (InfraredKeyId) -> Unit, onSend: (InfraredKeyId) -> Unit, onRename: (InfraredKeyId, String) -> Unit, @@ -265,6 +266,8 @@ internal fun InfraredControlScreen( if (showLearnDialog) { var keyName by remember { mutableStateOf("") } + var keySlot by remember { mutableStateOf(nextSlot.toString()) } + AlertDialog( onDismissRequest = { showLearnDialog = false }, title = { Text(stringResource(R.string.infrared_learn_dialog_title)) }, @@ -278,15 +281,22 @@ internal fun InfraredControlScreen( singleLine = true, modifier = Modifier.fillMaxWidth(), ) + OutlinedTextField( + value = keySlot, + onValueChange = { keySlot = it }, + label = { Text(stringResource(R.string.infrared_learn_dialog_slot_label)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) } }, confirmButton = { Button( onClick = { - onLearn(keyName) + onLearn(keyName, keySlot) showLearnDialog = false }, - enabled = keyName.isNotBlank(), + enabled = keyName.isNotBlank() && keySlot.isNotBlank(), ) { Text(stringResource(R.string.infrared_learn_dialog_start)) } @@ -387,7 +397,7 @@ private fun InfraredKeyRow( style = MaterialTheme.typography.titleMedium, ) Text( - text = "SDK: ${key.sdkKey}", + text = stringResource(R.string.infrared_key_slot_label, key.sdkKey), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlViewModel.kt b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlViewModel.kt index 7b5eeeb..d1ee47f 100644 --- a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlViewModel.kt +++ b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/ui/InfraredControlViewModel.kt @@ -192,7 +192,12 @@ class InfraredControlViewModel @Inject constructor( } } - fun learnKey(displayName: String) { + fun nextAvailableKeySlot(): Int { + val usedSlots = state.value.keys.mapNotNull { it.sdkKey.toIntOrNull() }.toSet() + return (1..100).firstOrNull { it !in usedSlots } ?: 1 + } + + fun learnKey(displayName: String, keySlotInput: String? = null) { val currentProfile = state.value.effectiveProfileId ?: return val savedDevice = state.value.savedDeviceName if (savedDevice == null) { @@ -204,7 +209,17 @@ class InfraredControlViewModel @Inject constructor( messageFlow.value = InfraredUiMessage.Error("Key name cannot be blank") return } - learningCoordinator.learn(currentProfile, savedDevice, trimmedName) + val slotString = if (keySlotInput != null && keySlotInput.isNotBlank()) { + val slotInt = keySlotInput.trim().toIntOrNull() + if (slotInt == null || slotInt !in 1..100) { + messageFlow.value = InfraredUiMessage.Error("Key slot must be an integer between 1 and 100") + return + } + slotInt.toString() + } else { + nextAvailableKeySlot().toString() + } + learningCoordinator.learn(currentProfile, savedDevice, trimmedName, slotString) } fun retryLearning(keyId: InfraredKeyId) { diff --git a/android/feature/infrared/src/main/res/values-zh-rCN/strings.xml b/android/feature/infrared/src/main/res/values-zh-rCN/strings.xml index 8592155..c166919 100644 --- a/android/feature/infrared/src/main/res/values-zh-rCN/strings.xml +++ b/android/feature/infrared/src/main/res/values-zh-rCN/strings.xml @@ -22,6 +22,8 @@ 学习红外按键 请准备好实体遥控器。点击开始后,请将遥控器对准红外控制器并按下目标按键。 按键名称(如:电源) + 按键编号(1-100) + 按键编号:%1$s 开始学习 重命名按键 新按键名称 diff --git a/android/feature/infrared/src/main/res/values/strings.xml b/android/feature/infrared/src/main/res/values/strings.xml index 42bd2f1..95304d2 100644 --- a/android/feature/infrared/src/main/res/values/strings.xml +++ b/android/feature/infrared/src/main/res/values/strings.xml @@ -22,6 +22,8 @@ Learn Infrared Key Prepare the physical remote. After starting, point it at the infrared controller and press the target button. Key Name (e.g. Power) + Key Slot (1-100) + Key Slot: %1$s Start Learning Rename Key New Display Name diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactory.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactory.kt index 0040df9..c83bd96 100644 --- a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactory.kt +++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactory.kt @@ -21,6 +21,7 @@ class InfraredKeyFactory( profileId: ProviderProfileId, deviceName: DeviceName, displayName: String, + sdkKey: String? = null, ): DomainResult { val normalizedName = displayName.trim() if ( @@ -39,6 +40,20 @@ class InfraredKeyFactory( ?: return DomainResult.Failure( DomainError.InvalidRequest(detail = "Infrared key id is invalid"), ) + + val finalSdkKey = if (sdkKey != null) { + val trimmed = sdkKey.trim() + val intVal = trimmed.toIntOrNull() + if (intVal == null || intVal !in 1..100) { + return DomainResult.Failure( + DomainError.InvalidRequest(detail = "Infrared key slot must be an integer between 1 and 100"), + ) + } + intVal.toString() + } else { + "app_ir_$canonicalId" + } + val now = clock.now() return DomainResult.Success( SavedInfraredKey( @@ -46,7 +61,7 @@ class InfraredKeyFactory( profileId = profileId, deviceName = deviceName, displayName = normalizedName, - sdkKey = "app_ir_$canonicalId", + sdkKey = finalSdkKey, status = InfraredKeyStatus.LEARNING, lastErrorCode = null, lastErrorDetail = null, @@ -68,4 +83,4 @@ class InfraredKeyFactory( "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$", ) } -} \ No newline at end of file +} diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/LearnInfraredKey.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/LearnInfraredKey.kt index 9f6155b..394557d 100644 --- a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/LearnInfraredKey.kt +++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/LearnInfraredKey.kt @@ -16,8 +16,9 @@ class LearnInfraredKey( profileId: ProviderProfileId, deviceName: DeviceName, displayName: String, + sdkKey: String? = null, ): DomainResult { - val key = when (val result = factory.create(profileId, deviceName, displayName)) { + val key = when (val result = factory.create(profileId, deviceName, displayName, sdkKey)) { is DomainResult.Success -> result.value is DomainResult.Failure -> return result } diff --git a/domain/src/test/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactoryTest.kt b/domain/src/test/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactoryTest.kt index dcc17e0..95c7b6b 100644 --- a/domain/src/test/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactoryTest.kt +++ b/domain/src/test/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactoryTest.kt @@ -32,6 +32,25 @@ class InfraredKeyFactoryTest { assertEquals(result.value.createdAtEpochMillis, result.value.updatedAtEpochMillis) } + @Test + fun createsLearningKeyWithCustomNumericSlot() { + val result = factory.create( + ProviderProfileId("abox"), + DeviceName("ir-1"), + "空调开", + sdkKey = "1", + ) as DomainResult.Success + + assertEquals("1", result.value.sdkKey) + } + + @Test + fun rejectsInvalidNumericSlots() { + assertTrue(factory.create(ProviderProfileId("abox"), DeviceName("ir-1"), "Power", sdkKey = "0") is DomainResult.Failure) + assertTrue(factory.create(ProviderProfileId("abox"), DeviceName("ir-1"), "Power", sdkKey = "101") is DomainResult.Failure) + assertTrue(factory.create(ProviderProfileId("abox"), DeviceName("ir-1"), "Power", sdkKey = "abc") is DomainResult.Failure) + } + @Test fun rejectsBlankDeviceOrDisplayName() { assertTrue(factory.create(ProviderProfileId("abox"), DeviceName(""), "Power") is DomainResult.Failure) @@ -67,4 +86,4 @@ class InfraredKeyFactoryTest { DeviceName("ir-1"), "Power", ) -} \ No newline at end of file +}