feat: add profile scoped device catalog
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
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.devicecatalog"
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) }
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
arguments { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":domain"))
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
kapt(libs.room.compiler)
|
||||
implementation(libs.hilt.android)
|
||||
kapt(libs.hilt.compiler)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.room.testing)
|
||||
androidTestImplementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.flagship.abox.manager.devicecatalog
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.flagship.abox.manager.devicecatalog.data.DeviceCatalogDatabase
|
||||
import com.flagship.abox.manager.devicecatalog.data.RoomConfiguredDeviceRepository
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DeviceType
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RoomConfiguredDeviceCatalogTest {
|
||||
private lateinit var database: DeviceCatalogDatabase
|
||||
private lateinit var repository: RoomConfiguredDeviceRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
database = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), DeviceCatalogDatabase::class.java).build()
|
||||
repository = RoomConfiguredDeviceRepository(database.configuredDeviceDao())
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() = database.close()
|
||||
|
||||
@Test
|
||||
fun configuredDevicesAreIsolatedByProfile() = runTest {
|
||||
repository.add(ProviderProfileId("one"), DeviceName("plug"), DeviceType.SOCKET)
|
||||
val result = repository.observeDevices(ProviderProfileId("two")).first() as DomainResult.Success
|
||||
assertTrue(result.value.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sameProfileRejectsDuplicateDeviceNameAcrossTypes() = runTest {
|
||||
val profile = ProviderProfileId("one")
|
||||
assertTrue(repository.add(profile, DeviceName("same"), DeviceType.SOCKET) is DomainResult.Success)
|
||||
assertTrue(repository.add(profile, DeviceName("same"), DeviceType.INFRARED) is DomainResult.Failure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeEmitsUpdatedCatalog() = runTest {
|
||||
val profile = ProviderProfileId("one")
|
||||
repository.add(profile, DeviceName("plug"), DeviceType.SOCKET)
|
||||
repository.remove(profile, DeviceName("plug"))
|
||||
val result = repository.observeDevices(profile).first() as DomainResult.Success
|
||||
assertEquals(emptyList<Any>(), result.value)
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.flagship.abox.manager.devicecatalog
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceCatalog
|
||||
import com.flagship.abox.manager.domain.DeviceDescriptor
|
||||
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.ProviderProfileStore
|
||||
import com.flagship.abox.manager.domain.ProviderType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class RoutingDeviceCatalog(
|
||||
private val profileStore: ProviderProfileStore,
|
||||
private val abox: DeviceCatalog,
|
||||
private val local: DeviceCatalog,
|
||||
) : DeviceCatalog {
|
||||
override fun observeDevices(profileId: ProviderProfileId): Flow<DomainResult<List<DeviceDescriptor>>> = flow {
|
||||
val active = profileStore.activeProfile()
|
||||
if (active?.id != profileId) {
|
||||
emit(DomainResult.Failure(DomainError.InvalidRequest(detail = "Provider profile is not active")))
|
||||
return@flow
|
||||
}
|
||||
emitAll(if (active.type == ProviderType.ABOX) abox.observeDevices(profileId) else local.observeDevices(profileId))
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.flagship.abox.manager.devicecatalog.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface ConfiguredDeviceDao {
|
||||
@Query("SELECT * FROM configured_devices WHERE profileId = :profileId ORDER BY deviceName")
|
||||
fun observe(profileId: String): Flow<List<ConfiguredDeviceEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
suspend fun insert(device: ConfiguredDeviceEntity)
|
||||
|
||||
@Query("DELETE FROM configured_devices WHERE profileId = :profileId AND deviceName = :deviceName")
|
||||
suspend fun remove(profileId: String, deviceName: String): Int
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.flagship.abox.manager.devicecatalog.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "configured_devices",
|
||||
indices = [Index(value = ["profileId", "deviceName"], unique = true)],
|
||||
)
|
||||
data class ConfiguredDeviceEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val profileId: String,
|
||||
val deviceName: String,
|
||||
val deviceType: String,
|
||||
)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.flagship.abox.manager.devicecatalog.data
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceCatalog
|
||||
import com.flagship.abox.manager.domain.DeviceDescriptor
|
||||
import com.flagship.abox.manager.domain.DeviceId
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DeviceType
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface ConfiguredDeviceRepository {
|
||||
fun observe(profileId: ProviderProfileId): Flow<DomainResult<List<DeviceDescriptor>>>
|
||||
suspend fun add(profileId: ProviderProfileId, name: DeviceName, type: DeviceType): DomainResult<DeviceDescriptor>
|
||||
suspend fun remove(profileId: ProviderProfileId, deviceName: DeviceName): DomainResult<Unit>
|
||||
}
|
||||
|
||||
class RoomConfiguredDeviceRepository(
|
||||
private val dao: ConfiguredDeviceDao,
|
||||
) : ConfiguredDeviceRepository, DeviceCatalog {
|
||||
override fun observe(profileId: ProviderProfileId): Flow<DomainResult<List<DeviceDescriptor>>> =
|
||||
dao.observe(profileId.value).map { entities -> DomainResult.Success(entities.map { it.toDomain() }) }
|
||||
|
||||
override fun observeDevices(profileId: ProviderProfileId): Flow<DomainResult<List<DeviceDescriptor>>> = observe(profileId)
|
||||
|
||||
override suspend fun add(profileId: ProviderProfileId, name: DeviceName, type: DeviceType): DomainResult<DeviceDescriptor> {
|
||||
if (profileId.value.isBlank() || name.value.isBlank()) {
|
||||
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Profile and device name are required"))
|
||||
}
|
||||
val descriptor = DeviceDescriptor(DeviceId("${profileId.value}:${name.value}"), profileId, name, type)
|
||||
return try {
|
||||
dao.insert(descriptor.toEntity())
|
||||
DomainResult.Success(descriptor)
|
||||
} catch (error: Exception) {
|
||||
DomainResult.Failure(DomainError.InvalidRequest(detail = error.message ?: "Duplicate device name"))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun remove(profileId: ProviderProfileId, deviceName: DeviceName): DomainResult<Unit> = try {
|
||||
dao.remove(profileId.value, deviceName.value)
|
||||
DomainResult.Success(Unit)
|
||||
} catch (error: Exception) {
|
||||
DomainResult.Failure(DomainError.Unknown(detail = error.message ?: "Unable to remove device"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun ConfiguredDeviceEntity.toDomain() = DeviceDescriptor(
|
||||
id = DeviceId(id),
|
||||
profileId = ProviderProfileId(profileId),
|
||||
name = DeviceName(deviceName),
|
||||
type = DeviceType.valueOf(deviceType),
|
||||
)
|
||||
|
||||
private fun DeviceDescriptor.toEntity() = ConfiguredDeviceEntity(
|
||||
id = id.value,
|
||||
profileId = profileId.value,
|
||||
deviceName = name.value,
|
||||
deviceType = type.name,
|
||||
)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.flagship.abox.manager.devicecatalog.data
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(entities = [ConfiguredDeviceEntity::class], version = 1, exportSchema = false)
|
||||
abstract class DeviceCatalogDatabase : RoomDatabase() {
|
||||
abstract fun configuredDeviceDao(): ConfiguredDeviceDao
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.flagship.abox.manager.devicecatalog.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.flagship.abox.manager.devicecatalog.data.ConfiguredDeviceRepository
|
||||
import com.flagship.abox.manager.devicecatalog.data.DeviceCatalogDatabase
|
||||
import com.flagship.abox.manager.devicecatalog.data.RoomConfiguredDeviceRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DeviceCatalogModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context: Context): DeviceCatalogDatabase =
|
||||
Room.databaseBuilder(context, DeviceCatalogDatabase::class.java, "abox_devices.db").build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRepository(database: DeviceCatalogDatabase): RoomConfiguredDeviceRepository =
|
||||
RoomConfiguredDeviceRepository(database.configuredDeviceDao())
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConfiguredDeviceRepository(repository: RoomConfiguredDeviceRepository): ConfiguredDeviceRepository = repository
|
||||
}
|
||||
Reference in New Issue
Block a user