feat: persist infrared key bindings
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.kapt)
|
||||
alias(libs.plugins.hilt.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.flagship.abox.manager.infrared"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
arguments {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":android:core:designsystem"))
|
||||
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
kapt(libs.room.compiler)
|
||||
|
||||
implementation(libs.datastore.preferences)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.hilt.android)
|
||||
kapt(libs.hilt.compiler)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
implementation(libs.compose.material3)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(project(":testing:fakes"))
|
||||
|
||||
androidTestImplementation(platform(libs.compose.bom))
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
androidTestImplementation(libs.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.room.testing)
|
||||
androidTestImplementation(libs.kotlinx.coroutines.test)
|
||||
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
debugImplementation(libs.compose.ui.test.manifest)
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "be673e12eb4c5275aaa6ef6fc189a861",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "infrared_keys",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `profileId` TEXT NOT NULL, `deviceName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `sdkKey` TEXT NOT NULL, `status` TEXT NOT NULL, `lastErrorCode` TEXT, `lastErrorDetail` TEXT, `createdAtEpochMillis` INTEGER NOT NULL, `updatedAtEpochMillis` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceName",
|
||||
"columnName": "deviceName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sdkKey",
|
||||
"columnName": "sdkKey",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastErrorCode",
|
||||
"columnName": "lastErrorCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastErrorDetail",
|
||||
"columnName": "lastErrorDetail",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAtEpochMillis",
|
||||
"columnName": "createdAtEpochMillis",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAtEpochMillis",
|
||||
"columnName": "updatedAtEpochMillis",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_infrared_keys_profileId_deviceName",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"deviceName"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_infrared_keys_profileId_deviceName` ON `${TABLE_NAME}` (`profileId`, `deviceName`)"
|
||||
},
|
||||
{
|
||||
"name": "index_infrared_keys_profileId_deviceName_sdkKey",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"deviceName",
|
||||
"sdkKey"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_infrared_keys_profileId_deviceName_sdkKey` ON `${TABLE_NAME}` (`profileId`, `deviceName`, `sdkKey`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "infrared_device_configuration",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `deviceName` TEXT NOT NULL, PRIMARY KEY(`profileId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceName",
|
||||
"columnName": "deviceName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be673e12eb4c5275aaa6ef6fc189a861')"
|
||||
]
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package com.flagship.abox.manager.infrared.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
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.INTERRUPTED_LEARNING_ERROR_DETAIL
|
||||
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.SavedInfraredKey
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RoomInfraredKeyRepositoryTest {
|
||||
|
||||
private lateinit var database: InfraredDatabase
|
||||
private lateinit var repository: RoomInfraredKeyRepository
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = Room.inMemoryDatabaseBuilder(context, InfraredDatabase::class.java)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
repository = RoomInfraredKeyRepository(database.infraredDao())
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun observesOnlyRequestedProfileAndDevice() = runTest {
|
||||
repository.insert(testKey(profile = "one", device = "ir-1", id = "a", sdkKey = "key_a"))
|
||||
repository.insert(testKey(profile = "one", device = "ir-2", id = "b", sdkKey = "key_b"))
|
||||
repository.insert(testKey(profile = "two", device = "ir-1", id = "c", sdkKey = "key_c"))
|
||||
|
||||
val result = repository.observeKeys(
|
||||
ProviderProfileId("one"),
|
||||
DeviceName("ir-1"),
|
||||
).first() as DomainResult.Success
|
||||
|
||||
assertEquals(listOf(InfraredKeyId("a")), result.value.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsDuplicateSdkBinding() = runTest {
|
||||
val first = repository.insert(testKey(id = "a", sdkKey = "app_ir_power"))
|
||||
assertTrue(first is DomainResult.Success)
|
||||
|
||||
val duplicate = repository.insert(testKey(id = "b", sdkKey = "app_ir_power"))
|
||||
assertTrue(duplicate is DomainResult.Failure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transitionsStatusCorrectly() = runTest {
|
||||
val key = testKey(id = "a", status = InfraredKeyStatus.LEARNING)
|
||||
repository.insert(key)
|
||||
|
||||
val transitionResult = repository.transitionStatus(
|
||||
ref = InfraredKeyRef(key.profileId, key.id),
|
||||
expectedStatuses = setOf(InfraredKeyStatus.LEARNING),
|
||||
targetStatus = InfraredKeyStatus.READY,
|
||||
lastErrorCode = null,
|
||||
lastErrorDetail = null,
|
||||
updatedAtEpochMillis = 2_000L,
|
||||
)
|
||||
assertTrue(transitionResult is DomainResult.Success)
|
||||
|
||||
val fetched = (repository.getKey(InfraredKeyRef(key.profileId, key.id)) as DomainResult.Success).value!!
|
||||
assertEquals(InfraredKeyStatus.READY, fetched.status)
|
||||
assertEquals(2_000L, fetched.updatedAtEpochMillis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renamesKeyWithoutChangingSdkKey() = runTest {
|
||||
val key = testKey(id = "a", displayName = "Old Name", sdkKey = "app_ir_power")
|
||||
repository.insert(key)
|
||||
|
||||
val renameResult = repository.rename(
|
||||
ref = InfraredKeyRef(key.profileId, key.id),
|
||||
displayName = "New Name",
|
||||
updatedAtEpochMillis = 3_000L,
|
||||
)
|
||||
assertTrue(renameResult is DomainResult.Success)
|
||||
|
||||
val fetched = (repository.getKey(InfraredKeyRef(key.profileId, key.id)) as DomainResult.Success).value!!
|
||||
assertEquals("New Name", fetched.displayName)
|
||||
assertEquals("app_ir_power", fetched.sdkKey)
|
||||
assertEquals(3_000L, fetched.updatedAtEpochMillis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteRejectsLearningKeyAndRemovesReadyKey() = runTest {
|
||||
val learningKey = testKey(id = "learning", status = InfraredKeyStatus.LEARNING)
|
||||
val readyKey = testKey(id = "ready", sdkKey = "ready_sdk_key", status = InfraredKeyStatus.READY)
|
||||
repository.insert(learningKey)
|
||||
repository.insert(readyKey)
|
||||
|
||||
val deleteLearning = repository.deleteIfNotLearning(InfraredKeyRef(learningKey.profileId, learningKey.id))
|
||||
assertTrue(deleteLearning is DomainResult.Failure)
|
||||
|
||||
val deleteReady = repository.deleteIfNotLearning(InfraredKeyRef(readyKey.profileId, readyKey.id))
|
||||
assertTrue(deleteReady is DomainResult.Success)
|
||||
|
||||
val fetchedReady = (repository.getKey(InfraredKeyRef(readyKey.profileId, readyKey.id)) as DomainResult.Success).value
|
||||
assertNull(fetchedReady)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failsInterruptedLearningKeys() = runTest {
|
||||
repository.insert(testKey(id = "k1", sdkKey = "k1", status = InfraredKeyStatus.LEARNING))
|
||||
repository.insert(testKey(id = "k2", sdkKey = "k2", status = InfraredKeyStatus.LEARNING))
|
||||
repository.insert(testKey(id = "k3", sdkKey = "k3", status = InfraredKeyStatus.READY))
|
||||
|
||||
val countResult = repository.failInterruptedLearning(ProviderProfileId("profile1"), 5_000L) as DomainResult.Success
|
||||
assertEquals(2, countResult.value)
|
||||
|
||||
val k1 = (repository.getKey(InfraredKeyRef(ProviderProfileId("profile1"), InfraredKeyId("k1"))) as DomainResult.Success).value!!
|
||||
assertEquals(InfraredKeyStatus.FAILED, k1.status)
|
||||
assertEquals(INTERRUPTED_LEARNING_ERROR_DETAIL, k1.lastErrorDetail)
|
||||
assertNull(k1.lastErrorCode)
|
||||
assertEquals(5_000L, k1.updatedAtEpochMillis)
|
||||
|
||||
val k3 = (repository.getKey(InfraredKeyRef(ProviderProfileId("profile1"), InfraredKeyId("k3"))) as DomainResult.Success).value!!
|
||||
assertEquals(InfraredKeyStatus.READY, k3.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun persistsAndObservesDeviceName() = runTest {
|
||||
val initial = repository.observeDeviceName(ProviderProfileId("profile1")).first() as DomainResult.Success
|
||||
assertNull(initial.value)
|
||||
|
||||
val setResult = repository.setDeviceName(ProviderProfileId("profile1"), DeviceName("ir_main"))
|
||||
assertTrue(setResult is DomainResult.Success)
|
||||
|
||||
val observed = repository.observeDeviceName(ProviderProfileId("profile1")).first() as DomainResult.Success
|
||||
assertEquals(DeviceName("ir_main"), observed.value)
|
||||
}
|
||||
|
||||
private fun testKey(
|
||||
profile: String = "profile1",
|
||||
device: String = "ir-1",
|
||||
id: String = "key-1",
|
||||
displayName: String = "Power",
|
||||
sdkKey: String = "app_ir_key1",
|
||||
status: InfraredKeyStatus = InfraredKeyStatus.LEARNING,
|
||||
): SavedInfraredKey {
|
||||
return SavedInfraredKey(
|
||||
id = InfraredKeyId(id),
|
||||
profileId = ProviderProfileId(profile),
|
||||
deviceName = DeviceName(device),
|
||||
displayName = displayName,
|
||||
sdkKey = sdkKey,
|
||||
status = status,
|
||||
lastErrorCode = null,
|
||||
lastErrorDetail = null,
|
||||
createdAtEpochMillis = 1_000L,
|
||||
updatedAtEpochMillis = 1_000L,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package com.flagship.abox.manager.infrared.data.local
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface InfraredDao {
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM infrared_keys
|
||||
WHERE profileId = :profileId AND deviceName = :deviceName
|
||||
ORDER BY createdAtEpochMillis ASC
|
||||
""",
|
||||
)
|
||||
fun observeKeys(profileId: String, deviceName: String): Flow<List<InfraredKeyEntity>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM infrared_keys
|
||||
WHERE profileId = :profileId AND id = :id
|
||||
LIMIT 1
|
||||
""",
|
||||
)
|
||||
suspend fun getKey(profileId: String, id: String): InfraredKeyEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
suspend fun insertKey(entity: InfraredKeyEntity)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE infrared_keys
|
||||
SET status = :targetStatus,
|
||||
lastErrorCode = :lastErrorCode,
|
||||
lastErrorDetail = :lastErrorDetail,
|
||||
updatedAtEpochMillis = :updatedAtEpochMillis
|
||||
WHERE profileId = :profileId
|
||||
AND id = :id
|
||||
AND status IN (:expectedStatuses)
|
||||
""",
|
||||
)
|
||||
suspend fun transitionStatus(
|
||||
profileId: String,
|
||||
id: String,
|
||||
expectedStatuses: List<String>,
|
||||
targetStatus: String,
|
||||
lastErrorCode: String?,
|
||||
lastErrorDetail: String?,
|
||||
updatedAtEpochMillis: Long,
|
||||
): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE infrared_keys
|
||||
SET displayName = :displayName,
|
||||
updatedAtEpochMillis = :updatedAtEpochMillis
|
||||
WHERE profileId = :profileId AND id = :id
|
||||
""",
|
||||
)
|
||||
suspend fun renameKey(
|
||||
profileId: String,
|
||||
id: String,
|
||||
displayName: String,
|
||||
updatedAtEpochMillis: Long,
|
||||
): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM infrared_keys
|
||||
WHERE profileId = :profileId AND id = :id AND status != 'LEARNING'
|
||||
""",
|
||||
)
|
||||
suspend fun deleteKeyIfNotLearning(profileId: String, id: String): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE infrared_keys
|
||||
SET status = :targetStatus,
|
||||
lastErrorCode = NULL,
|
||||
lastErrorDetail = :errorDetail,
|
||||
updatedAtEpochMillis = :updatedAtEpochMillis
|
||||
WHERE profileId = :profileId AND status = 'LEARNING'
|
||||
""",
|
||||
)
|
||||
suspend fun failInterruptedLearning(
|
||||
profileId: String,
|
||||
targetStatus: String,
|
||||
errorDetail: String,
|
||||
updatedAtEpochMillis: Long,
|
||||
): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM infrared_device_configuration
|
||||
WHERE profileId = :profileId
|
||||
LIMIT 1
|
||||
""",
|
||||
)
|
||||
fun observeDeviceName(profileId: String): Flow<InfraredDeviceConfigurationEntity?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun setDeviceName(entity: InfraredDeviceConfigurationEntity)
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.flagship.abox.manager.infrared.data.local
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
InfraredKeyEntity::class,
|
||||
InfraredDeviceConfigurationEntity::class,
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = true,
|
||||
)
|
||||
abstract class InfraredDatabase : RoomDatabase() {
|
||||
abstract fun infraredDao(): InfraredDao
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.flagship.abox.manager.infrared.data.local
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "infrared_keys",
|
||||
indices = [
|
||||
Index(value = ["profileId", "deviceName"]),
|
||||
Index(value = ["profileId", "deviceName", "sdkKey"], unique = true),
|
||||
],
|
||||
)
|
||||
data class InfraredKeyEntity(
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
val profileId: String,
|
||||
val deviceName: String,
|
||||
val displayName: String,
|
||||
val sdkKey: String,
|
||||
val status: String,
|
||||
val lastErrorCode: String?,
|
||||
val lastErrorDetail: String?,
|
||||
val createdAtEpochMillis: Long,
|
||||
val updatedAtEpochMillis: Long,
|
||||
)
|
||||
|
||||
@Entity(tableName = "infrared_device_configuration")
|
||||
data class InfraredDeviceConfigurationEntity(
|
||||
@PrimaryKey
|
||||
val profileId: String,
|
||||
val deviceName: String,
|
||||
)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.flagship.abox.manager.infrared.data.local
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyId
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyStatus
|
||||
import com.flagship.abox.manager.domain.infrared.SavedInfraredKey
|
||||
|
||||
fun InfraredKeyEntity.toDomain(): SavedInfraredKey {
|
||||
return SavedInfraredKey(
|
||||
id = InfraredKeyId(id),
|
||||
profileId = ProviderProfileId(profileId),
|
||||
deviceName = DeviceName(deviceName),
|
||||
displayName = displayName,
|
||||
sdkKey = sdkKey,
|
||||
status = InfraredKeyStatus.valueOf(status),
|
||||
lastErrorCode = lastErrorCode,
|
||||
lastErrorDetail = lastErrorDetail,
|
||||
createdAtEpochMillis = createdAtEpochMillis,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
}
|
||||
|
||||
fun SavedInfraredKey.toEntity(): InfraredKeyEntity {
|
||||
return InfraredKeyEntity(
|
||||
id = id.value,
|
||||
profileId = profileId.value,
|
||||
deviceName = deviceName.value,
|
||||
displayName = displayName,
|
||||
sdkKey = sdkKey,
|
||||
status = status.name,
|
||||
lastErrorCode = lastErrorCode,
|
||||
lastErrorDetail = lastErrorDetail,
|
||||
createdAtEpochMillis = createdAtEpochMillis,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package com.flagship.abox.manager.infrared.data.local
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.infrared.INTERRUPTED_LEARNING_ERROR_DETAIL
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredDeviceConfigurationRepository
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyRef
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyRepository
|
||||
import com.flagship.abox.manager.domain.infrared.InfraredKeyStatus
|
||||
import com.flagship.abox.manager.domain.infrared.SavedInfraredKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class RoomInfraredKeyRepository(
|
||||
private val dao: InfraredDao,
|
||||
) : InfraredKeyRepository, InfraredDeviceConfigurationRepository {
|
||||
|
||||
override fun observeKeys(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
): Flow<DomainResult<List<SavedInfraredKey>>> {
|
||||
return dao.observeKeys(profileId.value, deviceName.value)
|
||||
.map<List<InfraredKeyEntity>, DomainResult<List<SavedInfraredKey>>> { entities ->
|
||||
DomainResult.Success(entities.map { it.toDomain() })
|
||||
}
|
||||
.catch { throwable ->
|
||||
emit(DomainResult.Failure(mapThrowableToDomainError(throwable)))
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeDeviceName(profileId: ProviderProfileId): Flow<DomainResult<DeviceName?>> {
|
||||
return dao.observeDeviceName(profileId.value)
|
||||
.map<InfraredDeviceConfigurationEntity?, DomainResult<DeviceName?>> { entity ->
|
||||
DomainResult.Success(entity?.deviceName?.let { DeviceName(it) })
|
||||
}
|
||||
.catch { throwable ->
|
||||
emit(DomainResult.Failure(mapThrowableToDomainError(throwable)))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getKey(ref: InfraredKeyRef): DomainResult<SavedInfraredKey?> {
|
||||
return try {
|
||||
val entity = dao.getKey(ref.profileId.value, ref.keyId.value)
|
||||
DomainResult.Success(entity?.toDomain())
|
||||
} catch (throwable: Throwable) {
|
||||
DomainResult.Failure(mapThrowableToDomainError(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun insert(key: SavedInfraredKey): DomainResult<Unit> {
|
||||
return try {
|
||||
dao.insertKey(key.toEntity())
|
||||
DomainResult.Success(Unit)
|
||||
} catch (throwable: Throwable) {
|
||||
DomainResult.Failure(mapThrowableToDomainError(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun transitionStatus(
|
||||
ref: InfraredKeyRef,
|
||||
expectedStatuses: Set<InfraredKeyStatus>,
|
||||
targetStatus: InfraredKeyStatus,
|
||||
lastErrorCode: String?,
|
||||
lastErrorDetail: String?,
|
||||
updatedAtEpochMillis: Long,
|
||||
): DomainResult<Unit> {
|
||||
return try {
|
||||
val updated = dao.transitionStatus(
|
||||
profileId = ref.profileId.value,
|
||||
id = ref.keyId.value,
|
||||
expectedStatuses = expectedStatuses.map { it.name },
|
||||
targetStatus = targetStatus.name,
|
||||
lastErrorCode = lastErrorCode,
|
||||
lastErrorDetail = lastErrorDetail,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
if (updated > 0) {
|
||||
DomainResult.Success(Unit)
|
||||
} else {
|
||||
val existing = dao.getKey(ref.profileId.value, ref.keyId.value)
|
||||
if (existing == null) {
|
||||
DomainResult.Failure(DomainError.NoData(detail = "Infrared key not found"))
|
||||
} else {
|
||||
DomainResult.Failure(
|
||||
DomainError.InvalidRequest(
|
||||
detail = "Cannot transition key from ${existing.status} to ${targetStatus.name}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
DomainResult.Failure(mapThrowableToDomainError(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun rename(
|
||||
ref: InfraredKeyRef,
|
||||
displayName: String,
|
||||
updatedAtEpochMillis: Long,
|
||||
): DomainResult<Unit> {
|
||||
val trimmed = displayName.trim()
|
||||
if (trimmed.isBlank()) {
|
||||
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Display name cannot be blank"))
|
||||
}
|
||||
return try {
|
||||
val updated = dao.renameKey(
|
||||
profileId = ref.profileId.value,
|
||||
id = ref.keyId.value,
|
||||
displayName = trimmed,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
if (updated > 0) {
|
||||
DomainResult.Success(Unit)
|
||||
} else {
|
||||
DomainResult.Failure(DomainError.NoData(detail = "Infrared key not found"))
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
DomainResult.Failure(mapThrowableToDomainError(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteIfNotLearning(ref: InfraredKeyRef): DomainResult<Unit> {
|
||||
return try {
|
||||
val deleted = dao.deleteKeyIfNotLearning(ref.profileId.value, ref.keyId.value)
|
||||
if (deleted > 0) {
|
||||
DomainResult.Success(Unit)
|
||||
} else {
|
||||
val existing = dao.getKey(ref.profileId.value, ref.keyId.value)
|
||||
if (existing == null) {
|
||||
DomainResult.Failure(DomainError.NoData(detail = "Infrared key not found"))
|
||||
} else {
|
||||
DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Cannot delete key while learning is in progress"),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
DomainResult.Failure(mapThrowableToDomainError(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun failInterruptedLearning(
|
||||
profileId: ProviderProfileId,
|
||||
updatedAtEpochMillis: Long,
|
||||
): DomainResult<Int> {
|
||||
return try {
|
||||
val count = dao.failInterruptedLearning(
|
||||
profileId = profileId.value,
|
||||
targetStatus = InfraredKeyStatus.FAILED.name,
|
||||
errorDetail = INTERRUPTED_LEARNING_ERROR_DETAIL,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
DomainResult.Success(count)
|
||||
} catch (throwable: Throwable) {
|
||||
DomainResult.Failure(mapThrowableToDomainError(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setDeviceName(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
): DomainResult<Unit> {
|
||||
val trimmed = deviceName.value.trim()
|
||||
if (trimmed.isBlank()) {
|
||||
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Device name cannot be blank"))
|
||||
}
|
||||
return try {
|
||||
dao.setDeviceName(
|
||||
InfraredDeviceConfigurationEntity(
|
||||
profileId = profileId.value,
|
||||
deviceName = trimmed,
|
||||
),
|
||||
)
|
||||
DomainResult.Success(Unit)
|
||||
} catch (throwable: Throwable) {
|
||||
DomainResult.Failure(mapThrowableToDomainError(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapThrowableToDomainError(throwable: Throwable): DomainError {
|
||||
return when (throwable) {
|
||||
is SQLiteConstraintException -> DomainError.InvalidRequest(detail = "Database constraint violation")
|
||||
else -> DomainError.Unknown(detail = "Database operation failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user