feat(infrared): add infrared control, learning, and virtual remote module #9

Merged
KremeCN merged 41 commits from feat/moler-infrared-control into main 2026-08-25 14:51:34 +08:00
12 changed files with 775 additions and 1 deletions
Showing only changes of commit ff85b3eb45 - Show all commits
+76
View File
@@ -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)
}
@@ -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')"
]
}
}
@@ -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>
@@ -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)
}
@@ -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
}
@@ -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,
)
@@ -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,
)
}
@@ -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")
}
}
}
+1
View File
@@ -3,3 +3,4 @@ org.gradle.configuration-cache=false
android.useAndroidX=true android.useAndroidX=true
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
kotlin.code.style=official kotlin.code.style=official
android.overridePathCheck=true
+2 -1
View File
@@ -17,7 +17,6 @@ espresso = "3.6.1"
[libraries] [libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.2.0" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.2.0" }
@@ -35,6 +34,8 @@ hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt"
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
junit = { module = "junit:junit", version.ref = "junit" } junit = { module = "junit:junit", version.ref = "junit" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxJunit" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxJunit" }
+1
View File
@@ -28,4 +28,5 @@ include(":absdk-bridge")
include(":testing:fakes") include(":testing:fakes")
include(":android:core:designsystem") include(":android:core:designsystem")
include(":android:core:session") include(":android:core:session")
include(":android:feature:infrared")
include(":android:app") include(":android:app")