feat(device): add visibility/luftdata/narodmon settings, device model icons, SDK refactor
- Add SetDeviceVisibilityUseCase, SetLuftdataUseCase, SetNarodmonUseCase - Introduce DeviceModel enum (Basic, Mobile, Solar, Radiation, Custom) - Add device-type drawables (active/inactive icons for each model) - Refactor Device SDK: repository, DAO, database, local/remote data sources - Update DeviceSettingsScreen, ManageViewModel, theme colors - Add/update string resources (default, ru, be) Made-with: Cursor
This commit is contained in:
@@ -112,6 +112,87 @@ class DeviceRepositoryImpl @Inject constructor(
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun setNarodmon(deviceId: String, enabled: Boolean): Result<Unit> {
|
||||
val current = localDataSource.getDevice(deviceId) ?: return Result.failure(NoSuchElementException("Device not found: $deviceId"))
|
||||
val previous = current.isNarodmonOn
|
||||
|
||||
localDataSource.updateNarodmon(deviceId, enabled)
|
||||
val mutationId = UUID.randomUUID().toString()
|
||||
localDataSource.enqueuePendingMutation(
|
||||
PendingMutation(
|
||||
id = mutationId,
|
||||
type = PendingMutationType.NARODMON,
|
||||
deviceId = deviceId,
|
||||
payload = """{"enabled":$enabled}""",
|
||||
createdAt = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteDataSource.setNarodmon(deviceId, enabled)
|
||||
if (result.isSuccess) {
|
||||
localDataSource.removePendingMutationsForDevice(deviceId, PendingMutationType.NARODMON)
|
||||
} else {
|
||||
localDataSource.updateNarodmon(deviceId, previous)
|
||||
localDataSource.removePendingMutationsForDevice(deviceId, PendingMutationType.NARODMON)
|
||||
return result
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun setLuftdata(deviceId: String, enabled: Boolean): Result<Unit> {
|
||||
val current = localDataSource.getDevice(deviceId) ?: return Result.failure(NoSuchElementException("Device not found: $deviceId"))
|
||||
val previous = current.isLuftdataOn
|
||||
|
||||
localDataSource.updateLuftdata(deviceId, enabled)
|
||||
val mutationId = UUID.randomUUID().toString()
|
||||
localDataSource.enqueuePendingMutation(
|
||||
PendingMutation(
|
||||
id = mutationId,
|
||||
type = PendingMutationType.LUFTDATA,
|
||||
deviceId = deviceId,
|
||||
payload = """{"enabled":$enabled}""",
|
||||
createdAt = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteDataSource.setLuftdata(deviceId, enabled)
|
||||
if (result.isSuccess) {
|
||||
localDataSource.removePendingMutationsForDevice(deviceId, PendingMutationType.LUFTDATA)
|
||||
} else {
|
||||
localDataSource.updateLuftdata(deviceId, previous)
|
||||
localDataSource.removePendingMutationsForDevice(deviceId, PendingMutationType.LUFTDATA)
|
||||
return result
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun setVisibility(deviceId: String, isPublic: Boolean): Result<Unit> {
|
||||
val current = localDataSource.getDevice(deviceId) ?: return Result.failure(NoSuchElementException("Device not found: $deviceId"))
|
||||
val previous = current.isPublic
|
||||
|
||||
localDataSource.updateIsPublic(deviceId, isPublic)
|
||||
val mutationId = UUID.randomUUID().toString()
|
||||
localDataSource.enqueuePendingMutation(
|
||||
PendingMutation(
|
||||
id = mutationId,
|
||||
type = PendingMutationType.VISIBILITY,
|
||||
deviceId = deviceId,
|
||||
payload = """{"isPublic":$isPublic}""",
|
||||
createdAt = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteDataSource.setVisibility(deviceId, isPublic)
|
||||
if (result.isSuccess) {
|
||||
localDataSource.removePendingMutationsForDevice(deviceId, PendingMutationType.VISIBILITY)
|
||||
} else {
|
||||
localDataSource.updateIsPublic(deviceId, previous)
|
||||
localDataSource.removePendingMutationsForDevice(deviceId, PendingMutationType.VISIBILITY)
|
||||
return result
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun triggerFirmwareUpdate(deviceId: String): Result<Unit> =
|
||||
remoteDataSource.triggerFirmwareUpdate(deviceId)
|
||||
|
||||
|
||||
@@ -30,6 +30,15 @@ interface DeviceDao {
|
||||
@Query("UPDATE device SET dataSharingEnabled = :enabled WHERE id = :deviceId")
|
||||
suspend fun updateDataSharing(deviceId: String, enabled: Boolean)
|
||||
|
||||
@Query("UPDATE device SET isNarodmonOn = :enabled WHERE id = :deviceId")
|
||||
suspend fun updateNarodmon(deviceId: String, enabled: Boolean)
|
||||
|
||||
@Query("UPDATE device SET isLuftdataOn = :enabled WHERE id = :deviceId")
|
||||
suspend fun updateLuftdata(deviceId: String, enabled: Boolean)
|
||||
|
||||
@Query("UPDATE device SET isPublic = :isPublic WHERE id = :deviceId")
|
||||
suspend fun updateIsPublic(deviceId: String, isPublic: Boolean)
|
||||
|
||||
@Query("UPDATE device SET isOnline = :isOnline, isOnlineUpdatedAt = :updatedAt WHERE id = :deviceId")
|
||||
suspend fun updateOnlineStatus(deviceId: String, isOnline: Boolean, updatedAt: Long)
|
||||
|
||||
|
||||
@@ -2,10 +2,27 @@ package org.db3.airmq.sdk.device.data.local
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
val DEVICE_DB_MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE device ADD COLUMN deviceAddress TEXT NOT NULL DEFAULT ''")
|
||||
db.execSQL("ALTER TABLE device ADD COLUMN configVersion TEXT NOT NULL DEFAULT ''")
|
||||
db.execSQL("ALTER TABLE device ADD COLUMN isNarodmonOn INTEGER NOT NULL DEFAULT 0")
|
||||
db.execSQL("ALTER TABLE device ADD COLUMN isLuftdataOn INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val DEVICE_DB_MIGRATION_2_3 = object : Migration(2, 3) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE device ADD COLUMN isPublic INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
@Database(
|
||||
entities = [DeviceEntity::class, PendingMutationEntity::class],
|
||||
version = 1,
|
||||
version = 3,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class DeviceDatabase : RoomDatabase() {
|
||||
|
||||
@@ -18,9 +18,14 @@ data class DeviceEntity(
|
||||
val name: String,
|
||||
val model: String,
|
||||
val firmwareVersion: String,
|
||||
val deviceAddress: String,
|
||||
val configVersion: String,
|
||||
val isNarodmonOn: Boolean,
|
||||
val isLuftdataOn: Boolean,
|
||||
val locationId: String? = null,
|
||||
val latitude: Double? = null,
|
||||
val longitude: Double? = null,
|
||||
val isPublic: Boolean = false,
|
||||
val dataSharingEnabled: Boolean = false,
|
||||
val isOnline: Boolean = false,
|
||||
val isOnlineUpdatedAt: Long? = null,
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.db3.airmq.sdk.device.data.local
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.db3.airmq.sdk.device.domain.Device
|
||||
import org.db3.airmq.sdk.device.domain.DeviceModel
|
||||
import org.db3.airmq.sdk.device.domain.OnlineFreshness
|
||||
import org.db3.airmq.sdk.device.domain.PendingMutation
|
||||
import org.db3.airmq.sdk.device.domain.PendingMutationType
|
||||
@@ -47,6 +48,18 @@ class DeviceLocalDataSource @Inject constructor(
|
||||
deviceDao.updateDataSharing(deviceId, enabled)
|
||||
}
|
||||
|
||||
suspend fun updateNarodmon(deviceId: String, enabled: Boolean) {
|
||||
deviceDao.updateNarodmon(deviceId, enabled)
|
||||
}
|
||||
|
||||
suspend fun updateLuftdata(deviceId: String, enabled: Boolean) {
|
||||
deviceDao.updateLuftdata(deviceId, enabled)
|
||||
}
|
||||
|
||||
suspend fun updateIsPublic(deviceId: String, isPublic: Boolean) {
|
||||
deviceDao.updateIsPublic(deviceId, isPublic)
|
||||
}
|
||||
|
||||
suspend fun updateOnlineStatus(deviceId: String, isOnline: Boolean, updatedAt: Long) {
|
||||
deviceDao.updateOnlineStatus(deviceId, isOnline, updatedAt)
|
||||
}
|
||||
@@ -91,11 +104,16 @@ class DeviceLocalDataSource @Inject constructor(
|
||||
return Device(
|
||||
id = id,
|
||||
name = name,
|
||||
model = model,
|
||||
model = DeviceModel.fromString(model),
|
||||
firmwareVersion = firmwareVersion,
|
||||
deviceAddress = deviceAddress,
|
||||
configVersion = configVersion,
|
||||
isNarodmonOn = isNarodmonOn,
|
||||
isLuftdataOn = isLuftdataOn,
|
||||
locationId = locationId,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
isPublic = isPublic,
|
||||
city = city,
|
||||
dataSharingEnabled = dataSharingEnabled,
|
||||
isOnline = isOnline,
|
||||
@@ -107,11 +125,16 @@ class DeviceLocalDataSource @Inject constructor(
|
||||
private fun Device.toEntity(): DeviceEntity = DeviceEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
model = model,
|
||||
model = model.toStorageString(),
|
||||
firmwareVersion = firmwareVersion,
|
||||
deviceAddress = deviceAddress,
|
||||
configVersion = configVersion,
|
||||
isNarodmonOn = isNarodmonOn,
|
||||
isLuftdataOn = isLuftdataOn,
|
||||
locationId = locationId,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
isPublic = isPublic,
|
||||
dataSharingEnabled = dataSharingEnabled,
|
||||
isOnline = isOnline,
|
||||
isOnlineUpdatedAt = if (onlineFreshness == OnlineFreshness.Fresh) System.currentTimeMillis() else null,
|
||||
|
||||
@@ -4,6 +4,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.db3.airmq.sdk.device.domain.Device
|
||||
import org.db3.airmq.sdk.device.domain.DeviceModel
|
||||
import org.db3.airmq.sdk.device.domain.OnlineFreshness
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -39,6 +40,21 @@ interface DeviceRemoteDataSource {
|
||||
*/
|
||||
suspend fun setDataSharing(deviceId: String, enabled: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Execute Narodmon.ru toggle. Phase 1: No-op. Phase 2: Apollo mutation.
|
||||
*/
|
||||
suspend fun setNarodmon(deviceId: String, enabled: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Execute Sensor.community (Luftdata) toggle. Phase 1: No-op. Phase 2: Apollo mutation.
|
||||
*/
|
||||
suspend fun setLuftdata(deviceId: String, enabled: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Execute set visibility (publish/hide) mutation. Phase 1: No-op. Phase 2: Apollo mutation.
|
||||
*/
|
||||
suspend fun setVisibility(deviceId: String, isPublic: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Execute firmware update. Phase 1: No-op. Phase 2: Apollo mutation.
|
||||
*/
|
||||
@@ -56,11 +72,16 @@ class MockDeviceRemoteDataSource @Inject constructor() : DeviceRemoteDataSource
|
||||
Device(
|
||||
id = "device-1",
|
||||
name = "AirMQ #42",
|
||||
model = "mobile",
|
||||
model = DeviceModel.Mobile,
|
||||
firmwareVersion = "1.0",
|
||||
deviceAddress = "192.168.1.100",
|
||||
configVersion = "42",
|
||||
isNarodmonOn = true,
|
||||
isLuftdataOn = false,
|
||||
locationId = "loc-1",
|
||||
latitude = 53.9,
|
||||
longitude = 27.5,
|
||||
isPublic = false,
|
||||
city = "Minsk",
|
||||
dataSharingEnabled = true,
|
||||
isOnline = true,
|
||||
@@ -70,11 +91,16 @@ class MockDeviceRemoteDataSource @Inject constructor() : DeviceRemoteDataSource
|
||||
Device(
|
||||
id = "device-2",
|
||||
name = "AirMQ #17",
|
||||
model = "mobile",
|
||||
model = DeviceModel.Mobile,
|
||||
firmwareVersion = "1.0",
|
||||
deviceAddress = "192.168.1.101",
|
||||
configVersion = "41",
|
||||
isNarodmonOn = false,
|
||||
isLuftdataOn = false,
|
||||
locationId = null,
|
||||
latitude = null,
|
||||
longitude = null,
|
||||
isPublic = false,
|
||||
city = null,
|
||||
dataSharingEnabled = false,
|
||||
isOnline = false,
|
||||
@@ -102,6 +128,15 @@ class MockDeviceRemoteDataSource @Inject constructor() : DeviceRemoteDataSource
|
||||
override suspend fun setDataSharing(deviceId: String, enabled: Boolean): Result<Unit> =
|
||||
Result.success(Unit)
|
||||
|
||||
override suspend fun setNarodmon(deviceId: String, enabled: Boolean): Result<Unit> =
|
||||
Result.success(Unit)
|
||||
|
||||
override suspend fun setLuftdata(deviceId: String, enabled: Boolean): Result<Unit> =
|
||||
Result.success(Unit)
|
||||
|
||||
override suspend fun setVisibility(deviceId: String, isPublic: Boolean): Result<Unit> =
|
||||
Result.success(Unit)
|
||||
|
||||
override suspend fun triggerFirmwareUpdate(deviceId: String): Result<Unit> =
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import org.db3.airmq.sdk.device.data.DeviceRepositoryImpl
|
||||
import org.db3.airmq.sdk.device.data.local.DEVICE_DB_MIGRATION_1_2
|
||||
import org.db3.airmq.sdk.device.data.local.DEVICE_DB_MIGRATION_2_3
|
||||
import org.db3.airmq.sdk.device.data.local.DeviceDao
|
||||
import org.db3.airmq.sdk.device.data.local.DeviceDatabase
|
||||
import org.db3.airmq.sdk.device.data.local.PendingMutationDao
|
||||
@@ -28,7 +30,7 @@ object DeviceDatabaseModule {
|
||||
context,
|
||||
DeviceDatabase::class.java,
|
||||
"airmq_device_db"
|
||||
).build()
|
||||
).addMigrations(DEVICE_DB_MIGRATION_1_2, DEVICE_DB_MIGRATION_2_3).build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
||||
@@ -26,11 +26,16 @@ enum class OnlineFreshness {
|
||||
*
|
||||
* @param id Unique device identifier
|
||||
* @param name Display name
|
||||
* @param model Device model identifier
|
||||
* @param model Device model type
|
||||
* @param firmwareVersion Firmware version string
|
||||
* @param deviceAddress Device IP address
|
||||
* @param configVersion Config version string
|
||||
* @param isNarodmonOn Whether Narodmon.ru data sharing is enabled
|
||||
* @param isLuftdataOn Whether Sensor.community data sharing is enabled
|
||||
* @param locationId Optional location identifier
|
||||
* @param latitude Optional latitude
|
||||
* @param longitude Optional longitude
|
||||
* @param isPublic Whether device location is published (visible to others)
|
||||
* @param city Optional city name for the location
|
||||
* @param dataSharingEnabled Whether data sharing is enabled
|
||||
* @param isOnline Whether the device is currently online
|
||||
@@ -40,11 +45,16 @@ enum class OnlineFreshness {
|
||||
data class Device(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val model: String,
|
||||
val model: DeviceModel,
|
||||
val firmwareVersion: String,
|
||||
val deviceAddress: String,
|
||||
val configVersion: String,
|
||||
val isNarodmonOn: Boolean,
|
||||
val isLuftdataOn: Boolean,
|
||||
val locationId: String? = null,
|
||||
val latitude: Double? = null,
|
||||
val longitude: Double? = null,
|
||||
val isPublic: Boolean = false,
|
||||
val city: String? = null,
|
||||
val dataSharingEnabled: Boolean = false,
|
||||
val isOnline: Boolean = false,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.db3.airmq.sdk.device.domain
|
||||
|
||||
/**
|
||||
* Device model type. Maps to legacy numeric codes and string identifiers from API/DB.
|
||||
*/
|
||||
enum class DeviceModel(val displayName: String, private val storageValue: String) {
|
||||
Basic("Basic", "0"),
|
||||
Mobile("Mobile", "1"),
|
||||
Solar("Solar", "2"),
|
||||
Radiation("Radiation", "4"),
|
||||
Custom("Custom", "-1");
|
||||
|
||||
fun toStorageString(): String = storageValue
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String?): DeviceModel = when (value?.lowercase()) {
|
||||
"0", "basic" -> Basic
|
||||
"1", "mobile" -> Mobile
|
||||
"2", "solar" -> Solar
|
||||
"4", "radiation" -> Radiation
|
||||
else -> Custom
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,22 @@ interface DeviceRepository {
|
||||
*/
|
||||
suspend fun setDataSharing(deviceId: String, enabled: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Enable or disable Narodmon.ru data sharing.
|
||||
*/
|
||||
suspend fun setNarodmon(deviceId: String, enabled: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Enable or disable Sensor.community (Luftdata) data sharing.
|
||||
*/
|
||||
suspend fun setLuftdata(deviceId: String, enabled: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set device visibility (publish/hide location on map).
|
||||
* Only effective when device has location.
|
||||
*/
|
||||
suspend fun setVisibility(deviceId: String, isPublic: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Trigger firmware update. Requires connectivity.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,10 @@ package org.db3.airmq.sdk.device.domain
|
||||
enum class PendingMutationType {
|
||||
RENAME,
|
||||
LOCATION,
|
||||
DATA_SHARING
|
||||
DATA_SHARING,
|
||||
NARODMON,
|
||||
LUFTDATA,
|
||||
VISIBILITY
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user