feat: support 1-100 numeric infrared key slots and gateway login entry
This commit is contained in:
@@ -75,5 +75,3 @@ private fun AppSessionUiState.screenKey(): String = when (this) {
|
|||||||
is AppSessionUiState.Login -> "login:${profile.id.value}"
|
is AppSessionUiState.Login -> "login:${profile.id.value}"
|
||||||
is AppSessionUiState.Authenticated -> "authenticated:${profile.id.value}"
|
is AppSessionUiState.Authenticated -> "authenticated:${profile.id.value}"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
+6
-6
@@ -3,7 +3,6 @@ package com.flagship.abox.manager.infrared.ui
|
|||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.flagship.abox.manager.domain.DeviceName
|
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.domain.infrared.SavedInfraredKey
|
||||||
import com.flagship.abox.manager.infrared.runtime.InfraredRuntimeMode
|
import com.flagship.abox.manager.infrared.runtime.InfraredRuntimeMode
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -36,11 +34,12 @@ class InfraredControlScreenTest {
|
|||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
InfraredControlScreen(
|
InfraredControlScreen(
|
||||||
state = state,
|
state = state,
|
||||||
|
nextSlot = 1,
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onSessionInvalid = {},
|
onSessionInvalid = {},
|
||||||
onDeviceNameChanged = {},
|
onDeviceNameChanged = {},
|
||||||
onSaveDeviceName = {},
|
onSaveDeviceName = {},
|
||||||
onLearn = {},
|
onLearn = { _, _ -> },
|
||||||
onRetry = {},
|
onRetry = {},
|
||||||
onSend = {},
|
onSend = {},
|
||||||
onRename = { _, _ -> },
|
onRename = { _, _ -> },
|
||||||
@@ -63,7 +62,7 @@ class InfraredControlScreenTest {
|
|||||||
profileId = ProviderProfileId("abox"),
|
profileId = ProviderProfileId("abox"),
|
||||||
deviceName = DeviceName("ir-1"),
|
deviceName = DeviceName("ir-1"),
|
||||||
displayName = "Power Key",
|
displayName = "Power Key",
|
||||||
sdkKey = "app_ir_kready",
|
sdkKey = "1",
|
||||||
status = InfraredKeyStatus.READY,
|
status = InfraredKeyStatus.READY,
|
||||||
lastErrorCode = null,
|
lastErrorCode = null,
|
||||||
lastErrorDetail = null,
|
lastErrorDetail = null,
|
||||||
@@ -75,7 +74,7 @@ class InfraredControlScreenTest {
|
|||||||
profileId = ProviderProfileId("abox"),
|
profileId = ProviderProfileId("abox"),
|
||||||
deviceName = DeviceName("ir-1"),
|
deviceName = DeviceName("ir-1"),
|
||||||
displayName = "Volume Up",
|
displayName = "Volume Up",
|
||||||
sdkKey = "app_ir_kfailed",
|
sdkKey = "2",
|
||||||
status = InfraredKeyStatus.FAILED,
|
status = InfraredKeyStatus.FAILED,
|
||||||
lastErrorCode = "20206",
|
lastErrorCode = "20206",
|
||||||
lastErrorDetail = null,
|
lastErrorDetail = null,
|
||||||
@@ -96,11 +95,12 @@ class InfraredControlScreenTest {
|
|||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
InfraredControlScreen(
|
InfraredControlScreen(
|
||||||
state = state,
|
state = state,
|
||||||
|
nextSlot = 3,
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onSessionInvalid = {},
|
onSessionInvalid = {},
|
||||||
onDeviceNameChanged = {},
|
onDeviceNameChanged = {},
|
||||||
onSaveDeviceName = {},
|
onSaveDeviceName = {},
|
||||||
onLearn = {},
|
onLearn = { _, _ -> },
|
||||||
onRetry = { retriedKeyId = it },
|
onRetry = { retriedKeyId = it },
|
||||||
onSend = { sentKeyId = it },
|
onSend = { sentKeyId = it },
|
||||||
onRename = { _, _ -> },
|
onRename = { _, _ -> },
|
||||||
|
|||||||
+7
-2
@@ -13,9 +13,14 @@ class InfraredLearningCoordinator(
|
|||||||
private val retryInfraredKeyLearning: RetryInfraredKeyLearning,
|
private val retryInfraredKeyLearning: RetryInfraredKeyLearning,
|
||||||
private val externalScope: CoroutineScope,
|
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 {
|
externalScope.launch {
|
||||||
learnInfraredKey(profileId, deviceName, displayName)
|
learnInfraredKey(profileId, deviceName, displayName, sdkKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-5
@@ -1,6 +1,5 @@
|
|||||||
package com.flagship.abox.manager.infrared.ui
|
package com.flagship.abox.manager.infrared.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -58,6 +57,7 @@ fun InfraredControlRoute(
|
|||||||
|
|
||||||
InfraredControlScreen(
|
InfraredControlScreen(
|
||||||
state = state,
|
state = state,
|
||||||
|
nextSlot = viewModel.nextAvailableKeySlot(),
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onSessionInvalid = onSessionInvalid,
|
onSessionInvalid = onSessionInvalid,
|
||||||
onDeviceNameChanged = viewModel::onDeviceNameChanged,
|
onDeviceNameChanged = viewModel::onDeviceNameChanged,
|
||||||
@@ -76,11 +76,12 @@ fun InfraredControlRoute(
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun InfraredControlScreen(
|
internal fun InfraredControlScreen(
|
||||||
state: InfraredControlUiState,
|
state: InfraredControlUiState,
|
||||||
|
nextSlot: Int,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSessionInvalid: () -> Unit,
|
onSessionInvalid: () -> Unit,
|
||||||
onDeviceNameChanged: (String) -> Unit,
|
onDeviceNameChanged: (String) -> Unit,
|
||||||
onSaveDeviceName: () -> Unit,
|
onSaveDeviceName: () -> Unit,
|
||||||
onLearn: (String) -> Unit,
|
onLearn: (String, String?) -> Unit,
|
||||||
onRetry: (InfraredKeyId) -> Unit,
|
onRetry: (InfraredKeyId) -> Unit,
|
||||||
onSend: (InfraredKeyId) -> Unit,
|
onSend: (InfraredKeyId) -> Unit,
|
||||||
onRename: (InfraredKeyId, String) -> Unit,
|
onRename: (InfraredKeyId, String) -> Unit,
|
||||||
@@ -265,6 +266,8 @@ internal fun InfraredControlScreen(
|
|||||||
|
|
||||||
if (showLearnDialog) {
|
if (showLearnDialog) {
|
||||||
var keyName by remember { mutableStateOf("") }
|
var keyName by remember { mutableStateOf("") }
|
||||||
|
var keySlot by remember { mutableStateOf(nextSlot.toString()) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showLearnDialog = false },
|
onDismissRequest = { showLearnDialog = false },
|
||||||
title = { Text(stringResource(R.string.infrared_learn_dialog_title)) },
|
title = { Text(stringResource(R.string.infrared_learn_dialog_title)) },
|
||||||
@@ -278,15 +281,22 @@ internal fun InfraredControlScreen(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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 = {
|
confirmButton = {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
onLearn(keyName)
|
onLearn(keyName, keySlot)
|
||||||
showLearnDialog = false
|
showLearnDialog = false
|
||||||
},
|
},
|
||||||
enabled = keyName.isNotBlank(),
|
enabled = keyName.isNotBlank() && keySlot.isNotBlank(),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.infrared_learn_dialog_start))
|
Text(stringResource(R.string.infrared_learn_dialog_start))
|
||||||
}
|
}
|
||||||
@@ -387,7 +397,7 @@ private fun InfraredKeyRow(
|
|||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "SDK: ${key.sdkKey}",
|
text = stringResource(R.string.infrared_key_slot_label, key.sdkKey),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-2
@@ -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 currentProfile = state.value.effectiveProfileId ?: return
|
||||||
val savedDevice = state.value.savedDeviceName
|
val savedDevice = state.value.savedDeviceName
|
||||||
if (savedDevice == null) {
|
if (savedDevice == null) {
|
||||||
@@ -204,7 +209,17 @@ class InfraredControlViewModel @Inject constructor(
|
|||||||
messageFlow.value = InfraredUiMessage.Error("Key name cannot be blank")
|
messageFlow.value = InfraredUiMessage.Error("Key name cannot be blank")
|
||||||
return
|
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) {
|
fun retryLearning(keyId: InfraredKeyId) {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
<string name="infrared_learn_dialog_title">学习红外按键</string>
|
<string name="infrared_learn_dialog_title">学习红外按键</string>
|
||||||
<string name="infrared_learn_dialog_prompt">请准备好实体遥控器。点击开始后,请将遥控器对准红外控制器并按下目标按键。</string>
|
<string name="infrared_learn_dialog_prompt">请准备好实体遥控器。点击开始后,请将遥控器对准红外控制器并按下目标按键。</string>
|
||||||
<string name="infrared_learn_dialog_input_label">按键名称(如:电源)</string>
|
<string name="infrared_learn_dialog_input_label">按键名称(如:电源)</string>
|
||||||
|
<string name="infrared_learn_dialog_slot_label">按键编号(1-100)</string>
|
||||||
|
<string name="infrared_key_slot_label">按键编号:%1$s</string>
|
||||||
<string name="infrared_learn_dialog_start">开始学习</string>
|
<string name="infrared_learn_dialog_start">开始学习</string>
|
||||||
<string name="infrared_rename_dialog_title">重命名按键</string>
|
<string name="infrared_rename_dialog_title">重命名按键</string>
|
||||||
<string name="infrared_rename_dialog_input_label">新按键名称</string>
|
<string name="infrared_rename_dialog_input_label">新按键名称</string>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
<string name="infrared_learn_dialog_title">Learn Infrared Key</string>
|
<string name="infrared_learn_dialog_title">Learn Infrared Key</string>
|
||||||
<string name="infrared_learn_dialog_prompt">Prepare the physical remote. After starting, point it at the infrared controller and press the target button.</string>
|
<string name="infrared_learn_dialog_prompt">Prepare the physical remote. After starting, point it at the infrared controller and press the target button.</string>
|
||||||
<string name="infrared_learn_dialog_input_label">Key Name (e.g. Power)</string>
|
<string name="infrared_learn_dialog_input_label">Key Name (e.g. Power)</string>
|
||||||
|
<string name="infrared_learn_dialog_slot_label">Key Slot (1-100)</string>
|
||||||
|
<string name="infrared_key_slot_label">Key Slot: %1$s</string>
|
||||||
<string name="infrared_learn_dialog_start">Start Learning</string>
|
<string name="infrared_learn_dialog_start">Start Learning</string>
|
||||||
<string name="infrared_rename_dialog_title">Rename Key</string>
|
<string name="infrared_rename_dialog_title">Rename Key</string>
|
||||||
<string name="infrared_rename_dialog_input_label">New Display Name</string>
|
<string name="infrared_rename_dialog_input_label">New Display Name</string>
|
||||||
|
|||||||
+16
-1
@@ -21,6 +21,7 @@ class InfraredKeyFactory(
|
|||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
deviceName: DeviceName,
|
deviceName: DeviceName,
|
||||||
displayName: String,
|
displayName: String,
|
||||||
|
sdkKey: String? = null,
|
||||||
): DomainResult<SavedInfraredKey> {
|
): DomainResult<SavedInfraredKey> {
|
||||||
val normalizedName = displayName.trim()
|
val normalizedName = displayName.trim()
|
||||||
if (
|
if (
|
||||||
@@ -39,6 +40,20 @@ class InfraredKeyFactory(
|
|||||||
?: return DomainResult.Failure(
|
?: return DomainResult.Failure(
|
||||||
DomainError.InvalidRequest(detail = "Infrared key id is invalid"),
|
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()
|
val now = clock.now()
|
||||||
return DomainResult.Success(
|
return DomainResult.Success(
|
||||||
SavedInfraredKey(
|
SavedInfraredKey(
|
||||||
@@ -46,7 +61,7 @@ class InfraredKeyFactory(
|
|||||||
profileId = profileId,
|
profileId = profileId,
|
||||||
deviceName = deviceName,
|
deviceName = deviceName,
|
||||||
displayName = normalizedName,
|
displayName = normalizedName,
|
||||||
sdkKey = "app_ir_$canonicalId",
|
sdkKey = finalSdkKey,
|
||||||
status = InfraredKeyStatus.LEARNING,
|
status = InfraredKeyStatus.LEARNING,
|
||||||
lastErrorCode = null,
|
lastErrorCode = null,
|
||||||
lastErrorDetail = null,
|
lastErrorDetail = null,
|
||||||
|
|||||||
+2
-1
@@ -16,8 +16,9 @@ class LearnInfraredKey(
|
|||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
deviceName: DeviceName,
|
deviceName: DeviceName,
|
||||||
displayName: String,
|
displayName: String,
|
||||||
|
sdkKey: String? = null,
|
||||||
): DomainResult<InfraredKeyId> {
|
): DomainResult<InfraredKeyId> {
|
||||||
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.Success -> result.value
|
||||||
is DomainResult.Failure -> return result
|
is DomainResult.Failure -> return result
|
||||||
}
|
}
|
||||||
|
|||||||
+19
@@ -32,6 +32,25 @@ class InfraredKeyFactoryTest {
|
|||||||
assertEquals(result.value.createdAtEpochMillis, result.value.updatedAtEpochMillis)
|
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
|
@Test
|
||||||
fun rejectsBlankDeviceOrDisplayName() {
|
fun rejectsBlankDeviceOrDisplayName() {
|
||||||
assertTrue(factory.create(ProviderProfileId("abox"), DeviceName(""), "Power") is DomainResult.Failure)
|
assertTrue(factory.create(ProviderProfileId("abox"), DeviceName(""), "Power") is DomainResult.Failure)
|
||||||
|
|||||||
Reference in New Issue
Block a user