feat(infrared): refactor virtual remote hub with multi-device presets and quota management

This commit is contained in:
2026-08-25 14:11:49 +08:00
parent 86af7f4c91
commit 7f9fb8c2b6
29 changed files with 3072 additions and 44 deletions
@@ -129,6 +129,9 @@ class FakeInfraredGateway : InfraredGateway {
learnResults[InfraredOperationKey(profileId, deviceName, key)] = result
}
val downloadedCodes: List<InfraredCode>
get() = downloadCalls.flatMap { it.codes }
override suspend fun downloadCodes(
profileId: ProviderProfileId,
codes: List<InfraredCode>,
@@ -0,0 +1,126 @@
package com.flagship.abox.manager.testing.fakes
import com.flagship.abox.manager.domain.DeviceName
import com.flagship.abox.manager.domain.DomainResult
import com.flagship.abox.manager.domain.ProviderProfileId
import com.flagship.abox.manager.domain.infrared.AcStateSnapshot
import com.flagship.abox.manager.domain.infrared.RemoteKeyBinding
import com.flagship.abox.manager.domain.infrared.SlotQuotaSummary
import com.flagship.abox.manager.domain.infrared.VirtualRemote
import com.flagship.abox.manager.domain.infrared.VirtualRemoteId
import com.flagship.abox.manager.domain.infrared.VirtualRemoteRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import java.util.concurrent.ConcurrentHashMap
class FakeVirtualRemoteRepository : VirtualRemoteRepository {
private val remotes = ConcurrentHashMap<VirtualRemoteId, VirtualRemote>()
private val bindings = ConcurrentHashMap<VirtualRemoteId, MutableList<RemoteKeyBinding>>()
private val changeTrigger = MutableStateFlow(0L)
override fun observeRemotes(
profileId: ProviderProfileId,
deviceName: DeviceName,
): Flow<DomainResult<List<VirtualRemote>>> = changeTrigger.map {
DomainResult.Success(
remotes.values.filter { it.profileId == profileId && it.deviceName == deviceName }
.sortedByDescending { it.createdAtEpochMillis },
)
}
override fun observeRemote(
profileId: ProviderProfileId,
remoteId: VirtualRemoteId,
): Flow<DomainResult<VirtualRemote?>> = changeTrigger.map {
val remote = remotes[remoteId]?.takeIf { it.profileId == profileId }
DomainResult.Success(remote)
}
override fun observeKeyBindings(
remoteId: VirtualRemoteId,
): Flow<DomainResult<List<RemoteKeyBinding>>> = changeTrigger.map {
DomainResult.Success(bindings[remoteId]?.toList().orEmpty())
}
override fun observeSlotQuota(
profileId: ProviderProfileId,
deviceName: DeviceName,
): Flow<DomainResult<SlotQuotaSummary>> = changeTrigger.map {
val relevantBindings = remotes.values
.filter { it.profileId == profileId && it.deviceName == deviceName }
.flatMap { bindings[it.id].orEmpty() }
val usedSlots = relevantBindings.mapNotNull { it.assignedSlot?.toIntOrNull() }.toSet()
DomainResult.Success(
SlotQuotaSummary(
totalCapacity = 100,
dynamicScratchSlot = 100,
assignedStaticSlots = usedSlots,
),
)
}
override suspend fun getRemote(
profileId: ProviderProfileId,
remoteId: VirtualRemoteId,
): DomainResult<VirtualRemote?> {
val remote = remotes[remoteId]?.takeIf { it.profileId == profileId }
return DomainResult.Success(remote)
}
override suspend fun getKeyBindings(
remoteId: VirtualRemoteId,
): DomainResult<List<RemoteKeyBinding>> =
DomainResult.Success(bindings[remoteId]?.toList().orEmpty())
override suspend fun createRemote(
remote: VirtualRemote,
initialBindings: List<RemoteKeyBinding>,
): DomainResult<VirtualRemoteId> {
remotes[remote.id] = remote
bindings[remote.id] = initialBindings.toMutableList()
changeTrigger.value = System.currentTimeMillis()
return DomainResult.Success(remote.id)
}
override suspend fun updateRemote(remote: VirtualRemote): DomainResult<Unit> {
remotes[remote.id] = remote
changeTrigger.value = System.currentTimeMillis()
return DomainResult.Success(Unit)
}
override suspend fun deleteRemote(
profileId: ProviderProfileId,
remoteId: VirtualRemoteId,
): DomainResult<Unit> {
remotes.remove(remoteId)
bindings.remove(remoteId)
changeTrigger.value = System.currentTimeMillis()
return DomainResult.Success(Unit)
}
override suspend fun updateKeyBinding(binding: RemoteKeyBinding): DomainResult<Unit> {
val list = bindings[binding.remoteId] ?: mutableListOf()
val index = list.indexOfFirst { it.id == binding.id || it.buttonId == binding.buttonId }
if (index >= 0) {
list[index] = binding
} else {
list.add(binding)
}
bindings[binding.remoteId] = list
changeTrigger.value = System.currentTimeMillis()
return DomainResult.Success(Unit)
}
override suspend fun updateAcState(
profileId: ProviderProfileId,
remoteId: VirtualRemoteId,
state: AcStateSnapshot,
): DomainResult<Unit> {
val existing = remotes[remoteId] ?: return DomainResult.Success(Unit)
remotes[remoteId] = existing.copy(acState = state)
changeTrigger.value = System.currentTimeMillis()
return DomainResult.Success(Unit)
}
}