Feature/domain (#87)

* feature: Local

* feature: Local

* feature: Move analytics

* feature: Network

---------

Co-authored-by: TEYSSANDIER Raphael <rteyssandier@sephora.fr>
This commit is contained in:
Raphael Teyssandier 2025-08-11 21:21:11 +02:00 committed by GitHub
parent 9d73df5e5a
commit 466931e812
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 294 additions and 200 deletions

View file

@ -0,0 +1,19 @@
package io.github.openflocon.data.core.analytics.datasource
import io.github.openflocon.domain.analytics.models.AnalyticsIdentifierDomainModel
import io.github.openflocon.domain.analytics.models.AnalyticsItemDomainModel
import io.github.openflocon.domain.analytics.models.AnalyticsTableId
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import kotlinx.coroutines.flow.Flow
interface AnalyticsLocalDataSource {
suspend fun insert(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, items: List<AnalyticsItemDomainModel>)
fun observe(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, analyticsTableId: AnalyticsTableId): Flow<List<AnalyticsItemDomainModel>>
fun observeDeviceAnalytics(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow<List<AnalyticsIdentifierDomainModel>>
suspend fun getDeviceAnalytics(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): List<AnalyticsIdentifierDomainModel>
suspend fun delete(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
analyticsId: AnalyticsIdentifierDomainModel,
)
}

View file

@ -0,0 +1,15 @@
package io.github.openflocon.data.core.analytics.datasource
import io.github.openflocon.domain.analytics.models.AnalyticsIdentifierDomainModel
import io.github.openflocon.domain.analytics.models.AnalyticsTableId
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import kotlinx.coroutines.flow.Flow
interface DeviceAnalyticsDataSource {
fun observeSelectedDeviceAnalytics(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow<AnalyticsIdentifierDomainModel?>
fun selectDeviceAnalytics(
deviceAnalytics: List<AnalyticsIdentifierDomainModel>,
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
analyticsTableId: AnalyticsTableId,
)
}

View file

@ -0,0 +1,12 @@
package io.github.openflocon.data.core.dashboard.datasource
import io.github.openflocon.domain.dashboard.models.DashboardDomainModel
import io.github.openflocon.domain.dashboard.models.DashboardId
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import kotlinx.coroutines.flow.Flow
interface DashboardLocalDataSource {
suspend fun saveDashboard(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, dashboard: DashboardDomainModel)
fun observeDashboard(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, dashboardId: DashboardId): Flow<DashboardDomainModel?>
fun observeDeviceDashboards(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow<List<DashboardId>>
}

View file

@ -0,0 +1,18 @@
package io.github.openflocon.data.core.database.datasource
import io.github.openflocon.domain.database.models.DeviceDataBaseId
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import kotlinx.coroutines.flow.Flow
interface LocalDatabaseDataSource {
suspend fun saveSuccessQuery(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
databaseId: DeviceDataBaseId,
query: String,
)
fun observeLastSuccessQuery(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
databaseId: DeviceDataBaseId,
): Flow<List<String>>
}

View file

@ -0,0 +1,12 @@
package io.github.openflocon.data.core.deeplink.datasource
import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import kotlinx.coroutines.flow.Flow
interface DeeplinkLocalDataSource {
suspend fun update(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel, deeplinks: List<DeeplinkDomainModel>)
fun observe(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel): Flow<List<DeeplinkDomainModel>>
}

View file

@ -0,0 +1,19 @@
package io.github.openflocon.data.core.files.datasource
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.files.models.FileDomainModel
import io.github.openflocon.domain.files.models.FilePathDomainModel
import kotlinx.coroutines.flow.Flow
interface FilesLocalDataSource {
fun observeFolderContentUseCase(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
folderPath: FilePathDomainModel,
): Flow<List<FileDomainModel>>
suspend fun storeFiles(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
parentPath: FilePathDomainModel,
files: List<FileDomainModel>,
)
}

View file

@ -0,0 +1,15 @@
package io.github.openflocon.data.core.images.datasource
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.images.models.DeviceImageDomainModel
import kotlinx.coroutines.flow.Flow
interface ImagesLocalDataSource {
suspend fun addImage(
deviceId: DeviceId,
image: DeviceImageDomainModel,
)
fun observeImages(deviceId: DeviceId): Flow<List<DeviceImageDomainModel>>
suspend fun clearImages(deviceId: DeviceId)
}

View file

@ -0,0 +1,20 @@
package io.github.openflocon.data.core.network.datasource
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.network.models.NetworkTextFilterColumns
import io.github.openflocon.domain.models.TextFilterStateDomainModel
import kotlinx.coroutines.flow.Flow
interface NetworkFilterLocalDataSource {
suspend fun get(
deviceId: DeviceId,
column: NetworkTextFilterColumns,
): TextFilterStateDomainModel?
fun observe(deviceId: DeviceId): Flow<Map<NetworkTextFilterColumns, TextFilterStateDomainModel>>
suspend fun update(
deviceId: DeviceId,
column: NetworkTextFilterColumns,
newValue: TextFilterStateDomainModel,
)
}

View file

@ -0,0 +1,34 @@
package io.github.openflocon.data.core.network.datasource
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.network.models.FloconHttpRequestDomainModel
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import kotlinx.coroutines.flow.Flow
interface NetworkLocalDataSource {
fun observeRequests(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
lite: Boolean,
): Flow<List<FloconHttpRequestDomainModel>>
fun observeRequest(
deviceId: DeviceId,
requestId: String,
): Flow<FloconHttpRequestDomainModel?>
suspend fun save(
deviceId: DeviceId,
packageName: String,
request: FloconHttpRequestDomainModel,
)
suspend fun clearDeviceCalls(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel)
suspend fun deleteRequest(deviceId: DeviceId, requestId: String)
suspend fun deleteRequestsBefore(deviceId: DeviceId, requestId: String)
suspend fun clear()
}

View file

@ -0,0 +1,21 @@
package io.github.openflocon.data.core.sharedpreference.datasource
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.sharedpreference.models.DeviceSharedPreferenceId
import io.github.openflocon.domain.sharedpreference.models.SharedPreferenceRowDomainModel
import io.github.openflocon.domain.sharedpreference.models.SharedPreferenceValuesResponseDomainModel
import kotlinx.coroutines.flow.Flow
interface DeviceSharedPreferencesValuesDataSource {
fun onSharedPreferencesValuesReceived(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
received: SharedPreferenceValuesResponseDomainModel,
)
fun observe(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
sharedPreferenceId: DeviceSharedPreferenceId,
): Flow<List<SharedPreferenceRowDomainModel>>
}

View file

@ -0,0 +1,23 @@
package io.github.openflocon.data.core.table.datasource
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.table.models.TableDomainModel
import io.github.openflocon.domain.table.models.TableId
import io.github.openflocon.domain.table.models.TableIdentifierDomainModel
import kotlinx.coroutines.flow.Flow
interface TableLocalDataSource {
suspend fun insert(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, tablePartialInfos: List<TableDomainModel>)
fun observe(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, tableId: TableId): Flow<TableDomainModel?>
fun observeDeviceTables(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow<List<TableIdentifierDomainModel>>
suspend fun getDeviceTables(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): List<TableIdentifierDomainModel>
suspend fun delete(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
tableId: TableIdentifierDomainModel,
)
}

1
FloconDesktop/data/local/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,47 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.room)
alias(libs.plugins.ksp)
}
kotlin {
jvm("desktop")
compilerOptions {
// Pour Kotlin 1.9+
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
freeCompilerArgs.add("-Xcontext-parameters")
freeCompilerArgs.add("-Xcontext-sensitive-resolution")
}
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutinesCore)
implementation(libs.kotlinx.serializationJson)
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.androidx.room.runtime)
implementation(projects.domain)
implementation(projects.data.core)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}
dependencies {
ksp(libs.androidx.room.compiler)
}
room {
schemaDirectory("$projectDir/schemas")
}

View file

@ -0,0 +1,12 @@
package io.github.openflocon.data.local
import io.github.openflocon.data.local.analytics.analyticsModule
import io.github.openflocon.data.local.network.networkModule
import org.koin.dsl.module
val dataLocalModule = module {
includes(
analyticsModule,
networkModule
)
}

View file

@ -0,0 +1,14 @@
package io.github.openflocon.data.local.analytics
import io.github.openflocon.data.core.analytics.datasource.AnalyticsLocalDataSource
import io.github.openflocon.data.core.analytics.datasource.DeviceAnalyticsDataSource
import io.github.openflocon.data.local.analytics.datasource.AnalyticsLocalDataSourceRoom
import io.github.openflocon.data.local.analytics.datasource.DeviceAnalyticsDataSourceInMemory
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
internal val analyticsModule = module {
singleOf(::AnalyticsLocalDataSourceRoom) bind AnalyticsLocalDataSource::class
singleOf(::DeviceAnalyticsDataSourceInMemory) bind DeviceAnalyticsDataSource::class
}

View file

@ -0,0 +1,92 @@
package io.github.openflocon.data.local.analytics.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import io.github.openflocon.data.local.analytics.models.AnalyticsItemEntity
import io.github.openflocon.domain.device.models.DeviceId
import kotlinx.coroutines.flow.Flow
@Dao
interface FloconAnalyticsDao {
@Transaction
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
suspend fun insertAnalyticsItems(
analyticsItemEntities: List<AnalyticsItemEntity>,
)
@Query(
"""
SELECT *
FROM AnalyticsItemEntity
WHERE deviceId = :deviceId
AND analyticsTableId = :analyticsTableId
AND packageName = :packageName
LIMIT 1
""",
)
fun observeAnalytics(
deviceId: DeviceId,
packageName: String,
analyticsTableId: String,
): Flow<AnalyticsItemEntity?>
@Query(
"""
SELECT DISTINCT analyticsTableId
FROM AnalyticsItemEntity
WHERE deviceId = :deviceId
AND packageName = :packageName
""",
)
fun observeAnalyticsTableIdsForDevice(
deviceId: DeviceId,
packageName: String,
): Flow<List<String>>
@Query(
"""
SELECT DISTINCT analyticsTableId
FROM AnalyticsItemEntity
WHERE deviceId = :deviceId
AND packageName = :packageName
""",
)
suspend fun getAnalyticsForDevice(
deviceId: DeviceId,
packageName: String,
): List<String>
@Query(
"""
SELECT *
FROM AnalyticsItemEntity
WHERE analyticsTableId = :analyticsTableId
AND deviceId = :deviceId
AND packageName = :packageName
ORDER BY createdAt ASC
""",
)
fun observeAnalyticsItems(
deviceId: DeviceId,
packageName: String,
analyticsTableId: String,
): Flow<List<AnalyticsItemEntity>>
@Query(
"""
DELETE FROM AnalyticsItemEntity
WHERE analyticsTableId = :analyticsTableId
AND deviceId = :deviceId
AND packageName = :packageName
""",
)
suspend fun deleteAnalyticsContent(
deviceId: String,
packageName: String,
analyticsTableId: String,
)
}

View file

@ -0,0 +1,76 @@
package io.github.openflocon.data.local.analytics.datasource
import io.github.openflocon.data.core.analytics.datasource.AnalyticsLocalDataSource
import io.github.openflocon.data.local.analytics.dao.FloconAnalyticsDao
import io.github.openflocon.data.local.analytics.mapper.toAnalyticsDomain
import io.github.openflocon.data.local.analytics.mapper.toEntity
import io.github.openflocon.domain.analytics.models.AnalyticsIdentifierDomainModel
import io.github.openflocon.domain.analytics.models.AnalyticsItemDomainModel
import io.github.openflocon.domain.analytics.models.AnalyticsTableId
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class AnalyticsLocalDataSourceRoom(
private val analyticsDao: FloconAnalyticsDao,
private val dispatcherProvider: DispatcherProvider,
) : AnalyticsLocalDataSource {
override suspend fun insert(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, items: List<AnalyticsItemDomainModel>) {
withContext(dispatcherProvider.data) {
analyticsDao.insertAnalyticsItems(
items.map { item ->
item.toEntity(deviceIdAndPackageName)
},
)
}
}
override fun observe(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
analyticsTableId: AnalyticsTableId
): Flow<List<AnalyticsItemDomainModel>> = analyticsDao.observeAnalyticsItems(
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
analyticsTableId = analyticsTableId,
)
.map { it.map { toAnalyticsDomain(it) } }
.flowOn(dispatcherProvider.data)
override fun observeDeviceAnalytics(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow<List<AnalyticsIdentifierDomainModel>> =
analyticsDao.observeAnalyticsTableIdsForDevice(
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
)
.map { list ->
list.map {
AnalyticsIdentifierDomainModel(
id = it,
name = it,
)
}
}
override suspend fun delete(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, analyticsId: AnalyticsIdentifierDomainModel) {
analyticsDao.deleteAnalyticsContent(
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
analyticsTableId = analyticsId.id,
)
}
override suspend fun getDeviceAnalytics(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): List<AnalyticsIdentifierDomainModel> =
analyticsDao.getAnalyticsForDevice(
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
)
.map {
AnalyticsIdentifierDomainModel(
id = it,
name = it,
)
}
}

View file

@ -0,0 +1,32 @@
package io.github.openflocon.data.local.analytics.datasource
import io.github.openflocon.data.core.analytics.datasource.DeviceAnalyticsDataSource
import io.github.openflocon.domain.analytics.models.AnalyticsIdentifierDomainModel
import io.github.openflocon.domain.analytics.models.AnalyticsTableId
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
class DeviceAnalyticsDataSourceInMemory : DeviceAnalyticsDataSource {
private val selectedDeviceAnalytics = MutableStateFlow<Map<DeviceIdAndPackageNameDomainModel, AnalyticsIdentifierDomainModel?>>(emptyMap())
override fun observeSelectedDeviceAnalytics(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow<AnalyticsIdentifierDomainModel?> =
selectedDeviceAnalytics
.map { it[deviceIdAndPackageName] }
.distinctUntilChanged()
override fun selectDeviceAnalytics(
deviceAnalytics: List<AnalyticsIdentifierDomainModel>,
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
analyticsTableId: AnalyticsTableId,
) {
val analytics = deviceAnalytics.firstOrNull { it.id == analyticsTableId } ?: return
selectedDeviceAnalytics.update {
it + (deviceIdAndPackageName to analytics)
}
}
}

View file

@ -0,0 +1,19 @@
package io.github.openflocon.data.local.analytics.mapper
import io.github.openflocon.data.local.analytics.models.AnalyticsItemEntity
import io.github.openflocon.domain.analytics.models.AnalyticsItemDomainModel
import io.github.openflocon.domain.analytics.models.AnalyticsPropertyDomainModel
fun toAnalyticsDomain(entity: AnalyticsItemEntity): AnalyticsItemDomainModel = AnalyticsItemDomainModel(
analyticsTableId = entity.analyticsTableId,
itemId = entity.itemId,
createdAt = entity.createdAt,
eventName = entity.eventName,
properties = entity.propertiesValues.mapIndexedNotNull { index, value ->
AnalyticsPropertyDomainModel(
name = entity.propertiesColumnsNames.getOrNull(index)
?: return@mapIndexedNotNull null,
value = value,
)
},
)

View file

@ -0,0 +1,18 @@
package io.github.openflocon.data.local.analytics.mapper
import io.github.openflocon.data.local.analytics.models.AnalyticsItemEntity
import io.github.openflocon.domain.analytics.models.AnalyticsItemDomainModel
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
internal fun AnalyticsItemDomainModel.toEntity(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
) = AnalyticsItemEntity(
analyticsTableId = analyticsTableId,
createdAt = createdAt,
eventName = eventName,
itemId = itemId,
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
propertiesValues = properties.map { it.value },
propertiesColumnsNames = properties.map { it.name },
)

View file

@ -0,0 +1,22 @@
package io.github.openflocon.data.local.analytics.models
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
indices = [
Index(value = ["deviceId", "packageName", "analyticsTableId"]),
],
)
data class AnalyticsItemEntity(
@PrimaryKey
val itemId: String,
val analyticsTableId: String,
val deviceId: String,
val packageName: String,
val createdAt: Long,
val eventName: String,
val propertiesColumnsNames: List<String>,
val propertiesValues: List<String>,
)

View file

@ -0,0 +1,14 @@
package io.github.openflocon.data.local.network
import io.github.openflocon.data.core.network.datasource.NetworkFilterLocalDataSource
import io.github.openflocon.data.core.network.datasource.NetworkLocalDataSource
import io.github.openflocon.data.local.network.datasource.NetworkFilterLocalDataSourceRoom
import io.github.openflocon.data.local.network.datasource.NetworkLocalDataSourceRoom
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
internal val networkModule = module {
singleOf(::NetworkLocalDataSourceRoom) bind NetworkLocalDataSource::class
singleOf(::NetworkFilterLocalDataSourceRoom) bind NetworkFilterLocalDataSource::class
}

View file

@ -0,0 +1,98 @@
package io.github.openflocon.data.local.network.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntity
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntityLite
import io.github.openflocon.domain.device.models.DeviceId
import kotlinx.coroutines.flow.Flow
@Dao
interface FloconHttpRequestDao {
@Query(
"""
SELECT *
FROM FloconHttpRequestEntity
WHERE deviceId = :deviceId
AND packageName = :packageName
ORDER BY startTime ASC
""",
)
fun observeRequests(deviceId: String, packageName: String): Flow<List<FloconHttpRequestEntity>>
@Query(
"""
SELECT *
FROM FloconHttpRequestEntity
WHERE deviceId = :deviceId
AND packageName = :packageName
ORDER BY startTime ASC
""",
)
fun observeRequestsLite(
deviceId: String,
packageName: String,
): Flow<List<FloconHttpRequestEntityLite>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertRequest(request: FloconHttpRequestEntity)
@Query(
"""
SELECT *
FROM FloconHttpRequestEntity
WHERE deviceId = :deviceId AND uuid = :requestId
""",
)
fun observeRequestById(
deviceId: String,
requestId: String,
): Flow<FloconHttpRequestEntity?>
@Query("DELETE FROM FloconHttpRequestEntity")
suspend fun clearAll()
@Query(
"""
DELETE FROM FloconHttpRequestEntity
WHERE deviceId = :deviceId
AND packageName = :packageName
""",
)
suspend fun clearDeviceCalls(
deviceId: DeviceId,
packageName: String,
)
@Query(
"""
DELETE FROM FloconHttpRequestEntity
WHERE deviceId = :deviceId
AND uuid = :requestId
""",
)
suspend fun deleteRequest(
deviceId: String,
requestId: String,
)
@Query(
"""
DELETE FROM FloconHttpRequestEntity
WHERE deviceId = :deviceId
AND startTime < (
SELECT startTime
FROM FloconHttpRequestEntity
WHERE uuid = :requestId
AND deviceId = :deviceId
LIMIT 1
)
""",
)
suspend fun deleteRequestBefore(
deviceId: String,
requestId: String,
)
}

View file

@ -0,0 +1,21 @@
package io.github.openflocon.data.local.network.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.github.openflocon.data.local.network.models.NetworkFilterEntity
import io.github.openflocon.domain.network.models.NetworkTextFilterColumns
import kotlinx.coroutines.flow.Flow
@Dao
interface NetworkFilterDao {
@Query("SELECT * FROM network_filter WHERE deviceId = :deviceId AND columnName = :column")
suspend fun get(deviceId: String, column: NetworkTextFilterColumns): NetworkFilterEntity?
@Query("SELECT * FROM network_filter WHERE deviceId = :deviceId")
fun observe(deviceId: String): Flow<List<NetworkFilterEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdate(entity: NetworkFilterEntity)
}

View file

@ -0,0 +1,48 @@
package io.github.openflocon.data.local.network.datasource
import io.github.openflocon.data.core.network.datasource.NetworkFilterLocalDataSource
import io.github.openflocon.data.local.network.dao.NetworkFilterDao
import io.github.openflocon.data.local.network.mapper.textFilterToDomain
import io.github.openflocon.data.local.network.mapper.textFilterToEntity
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.models.TextFilterStateDomainModel
import io.github.openflocon.domain.network.models.NetworkTextFilterColumns
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class NetworkFilterLocalDataSourceRoom(
private val networkFilterDao: NetworkFilterDao,
) : NetworkFilterLocalDataSource {
override suspend fun get(
deviceId: DeviceId,
column: NetworkTextFilterColumns,
): TextFilterStateDomainModel? = networkFilterDao.get(
deviceId = deviceId,
column = column,
)?.let {
textFilterToDomain(it)
}
override fun observe(deviceId: DeviceId): Flow<Map<NetworkTextFilterColumns, TextFilterStateDomainModel>> = networkFilterDao.observe(
deviceId = deviceId,
).map {
it.associate {
it.columnName to textFilterToDomain(it)
}
}
override suspend fun update(
deviceId: DeviceId,
column: NetworkTextFilterColumns,
newValue: TextFilterStateDomainModel,
) {
networkFilterDao.insertOrUpdate(
textFilterToEntity(
deviceId = deviceId,
column = column,
domain = newValue,
),
)
}
}

View file

@ -0,0 +1,94 @@
package io.github.openflocon.data.local.network.datasource
import io.github.openflocon.data.core.network.datasource.NetworkLocalDataSource
import io.github.openflocon.data.local.network.dao.FloconHttpRequestDao
import io.github.openflocon.data.local.network.mapper.toDomainModel
import io.github.openflocon.data.local.network.mapper.toEntity
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntity
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntityLite
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.network.models.FloconHttpRequestDomainModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class NetworkLocalDataSourceRoom(
private val dispatcherProvider: DispatcherProvider,
private val floconHttpRequestDao: FloconHttpRequestDao
) : NetworkLocalDataSource {
override fun observeRequests(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
lite: Boolean,
): Flow<List<FloconHttpRequestDomainModel>> = floconHttpRequestDao.let {
if (lite) {
it.observeRequestsLite(
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
)
.map { entities -> entities.mapNotNull(FloconHttpRequestEntityLite::toDomainModel) }
} else {
it.observeRequests(
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
)
.map { entities -> entities.mapNotNull(FloconHttpRequestEntity::toDomainModel) }
}
}
.flowOn(dispatcherProvider.data)
override suspend fun save(
deviceId: DeviceId,
packageName: String,
request: FloconHttpRequestDomainModel,
) {
withContext(dispatcherProvider.data) {
val entity = request.toEntity(deviceId = deviceId, packageName = packageName)
floconHttpRequestDao.upsertRequest(entity)
}
}
override fun observeRequest(
deviceId: DeviceId,
requestId: String,
): Flow<FloconHttpRequestDomainModel?> = floconHttpRequestDao
.observeRequestById(deviceId, requestId)
.map { entity ->
entity?.toDomainModel()
}.flowOn(dispatcherProvider.data)
override suspend fun clearDeviceCalls(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel) {
withContext(dispatcherProvider.data) {
floconHttpRequestDao.clearDeviceCalls(
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
)
}
}
override suspend fun deleteRequest(
deviceId: DeviceId,
requestId: String,
) {
floconHttpRequestDao.deleteRequest(
requestId = requestId,
deviceId = deviceId,
)
}
override suspend fun deleteRequestsBefore(deviceId: DeviceId, requestId: String) {
floconHttpRequestDao.deleteRequestBefore(
requestId = requestId,
deviceId = deviceId,
)
}
override suspend fun clear() {
withContext(dispatcherProvider.data) {
floconHttpRequestDao.clearAll()
}
}
}

View file

@ -0,0 +1,48 @@
package io.github.openflocon.data.local.network.mapper
import io.github.openflocon.data.local.network.models.FilterItemSavedEntity
import io.github.openflocon.data.local.network.models.NetworkFilterEntity
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.models.TextFilterStateDomainModel
import io.github.openflocon.domain.network.models.NetworkTextFilterColumns
import kotlinx.serialization.json.Json
fun textFilterItemToEntity(item: TextFilterStateDomainModel.FilterItem): FilterItemSavedEntity = FilterItemSavedEntity(
text = item.text,
isActive = item.isActive,
isExcluded = item.isExcluded,
)
fun textFilterItemToDomain(item: FilterItemSavedEntity): TextFilterStateDomainModel.FilterItem = TextFilterStateDomainModel.FilterItem(
text = item.text,
isActive = item.isActive,
isExcluded = item.isExcluded,
)
fun textFilterToEntity(
deviceId: DeviceId,
column: NetworkTextFilterColumns,
domain: TextFilterStateDomainModel,
): NetworkFilterEntity {
val itemsEntity: List<FilterItemSavedEntity> = domain.items.map {
textFilterItemToEntity(it)
}
return NetworkFilterEntity(
deviceId = deviceId,
columnName = column,
isEnabled = domain.isEnabled,
itemsAsJson = Json.encodeToString(itemsEntity),
)
}
fun textFilterToDomain(
entity: NetworkFilterEntity,
): TextFilterStateDomainModel {
val itemsEntity = Json.decodeFromString<List<FilterItemSavedEntity>>(entity.itemsAsJson)
return TextFilterStateDomainModel(
isEnabled = entity.isEnabled,
items = itemsEntity.map {
textFilterItemToDomain(it)
},
)
}

View file

@ -0,0 +1,141 @@
package io.github.openflocon.data.local.network.mapper
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntity
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntityLite
import io.github.openflocon.data.local.network.models.FloconHttpRequestInfosEntity
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntityGraphQlEmbedded
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntityGrpcEmbedded
import io.github.openflocon.data.local.network.models.FloconHttpRequestEntityHttpEmbedded
import io.github.openflocon.domain.network.models.FloconHttpRequestDomainModel
fun FloconHttpRequestDomainModel.toEntity(
deviceId: String,
packageName: String,
): FloconHttpRequestEntity = FloconHttpRequestEntity(
uuid = this.uuid,
infos = this.toInfosEntity(),
deviceId = deviceId,
http = when (val t = this.type) {
is FloconHttpRequestDomainModel.Type.Http -> FloconHttpRequestEntityHttpEmbedded(
responseHttpCode = t.httpCode,
)
is FloconHttpRequestDomainModel.Type.GraphQl,
is FloconHttpRequestDomainModel.Type.Grpc,
-> null
},
graphql = when (val t = this.type) {
is FloconHttpRequestDomainModel.Type.GraphQl -> FloconHttpRequestEntityGraphQlEmbedded(
query = t.query,
operationType = t.operationType,
isSuccess = t.isSuccess,
responseHttpCode = t.httpCode,
)
is FloconHttpRequestDomainModel.Type.Http,
is FloconHttpRequestDomainModel.Type.Grpc,
-> null
},
grpc = when (val t = this.type) {
is FloconHttpRequestDomainModel.Type.Grpc -> FloconHttpRequestEntityGrpcEmbedded(
responseStatus = t.responseStatus,
)
is FloconHttpRequestDomainModel.Type.Http,
is FloconHttpRequestDomainModel.Type.GraphQl,
-> null
},
packageName = packageName,
)
private fun FloconHttpRequestDomainModel.toInfosEntity(): FloconHttpRequestInfosEntity = FloconHttpRequestInfosEntity(
url = this.url,
method = this.request.method,
startTime = this.request.startTime,
durationMs = this.durationMs,
requestHeaders = this.request.headers,
requestBody = this.request.body,
requestByteSize = this.request.byteSize,
responseContentType = this.response.contentType,
responseBody = this.response.body,
responseHeaders = this.response.headers,
responseByteSize = this.response.byteSize,
)
fun FloconHttpRequestEntity.toDomainModel(): FloconHttpRequestDomainModel? {
return FloconHttpRequestDomainModel(
uuid = this.uuid,
url = this.infos.url,
durationMs = this.infos.durationMs,
request = FloconHttpRequestDomainModel.Request(
method = this.infos.method,
startTime = this.infos.startTime,
headers = this.infos.requestHeaders,
body = this.infos.requestBody,
byteSize = this.infos.requestByteSize,
),
response = FloconHttpRequestDomainModel.Response(
contentType = this.infos.responseContentType,
body = this.infos.responseBody,
headers = this.infos.responseHeaders,
byteSize = this.infos.responseByteSize,
),
type = when {
this.graphql != null -> FloconHttpRequestDomainModel.Type.GraphQl(
query = this.graphql.query,
operationType = this.graphql.operationType,
isSuccess = this.graphql.isSuccess,
httpCode = this.graphql.responseHttpCode,
)
this.http != null -> FloconHttpRequestDomainModel.Type.Http(
httpCode = this.http.responseHttpCode,
)
this.grpc != null -> FloconHttpRequestDomainModel.Type.Grpc(
responseStatus = this.grpc.responseStatus,
)
else -> return null
},
)
}
fun FloconHttpRequestEntityLite.toDomainModel(): FloconHttpRequestDomainModel? {
return FloconHttpRequestDomainModel(
uuid = this.uuid,
url = this.url,
durationMs = this.durationMs,
request = FloconHttpRequestDomainModel.Request(
method = this.method,
startTime = this.startTime,
headers = emptyMap(), // removed for lite
body = null, // removed for lite
byteSize = 0L, // removed for lite
),
response = FloconHttpRequestDomainModel.Response(
contentType = null, // removed for lite
body = null, // removed for lite
headers = emptyMap(), // removed for lite
byteSize = 0L, // removed for lite
),
type = when {
this.graphql != null -> FloconHttpRequestDomainModel.Type.GraphQl(
query = this.graphql.query,
operationType = this.graphql.operationType,
isSuccess = this.graphql.isSuccess,
httpCode = this.graphql.responseHttpCode,
)
this.http != null -> FloconHttpRequestDomainModel.Type.Http(
httpCode = this.http.responseHttpCode,
)
this.grpc != null -> FloconHttpRequestDomainModel.Type.Grpc(
responseStatus = this.grpc.responseStatus,
)
else -> return null
},
)
}

View file

@ -0,0 +1,10 @@
package io.github.openflocon.data.local.network.models
import kotlinx.serialization.Serializable
@Serializable
data class FilterItemSavedEntity(
val text: String,
val isActive: Boolean,
val isExcluded: Boolean,
)

View file

@ -0,0 +1,46 @@
package io.github.openflocon.data.local.network.models
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
indices = [
Index(value = ["deviceId", "packageName"]),
],
)
data class FloconHttpRequestEntity(
@PrimaryKey
val uuid: String,
val deviceId: String, // To associate with a device
val packageName: String,
@Embedded
val infos: FloconHttpRequestInfosEntity,
// if it's a graphql method, this item is not null
@Embedded(prefix = "graphql_")
val graphql: FloconHttpRequestEntityGraphQlEmbedded?,
@Embedded(prefix = "http_")
val http: FloconHttpRequestEntityHttpEmbedded?,
@Embedded(prefix = "grpc_")
val grpc: FloconHttpRequestEntityGrpcEmbedded?,
)
data class FloconHttpRequestInfosEntity(
val url: String,
val method: String,
val startTime: Long,
val durationMs: Double,
val requestHeaders: Map<String, String>,
val requestBody: String?,
val requestByteSize: Long,
val responseContentType: String?,
val responseBody: String?,
val responseHeaders: Map<String, String>,
val responseByteSize: Long,
)

View file

@ -0,0 +1,8 @@
package io.github.openflocon.data.local.network.models
data class FloconHttpRequestEntityGraphQlEmbedded(
val query: String,
val operationType: String,
val isSuccess: Boolean,
val responseHttpCode: Int,
)

View file

@ -0,0 +1,5 @@
package io.github.openflocon.data.local.network.models
data class FloconHttpRequestEntityGrpcEmbedded(
val responseStatus: String,
)

View file

@ -0,0 +1,5 @@
package io.github.openflocon.data.local.network.models
data class FloconHttpRequestEntityHttpEmbedded(
val responseHttpCode: Int,
)

View file

@ -0,0 +1,29 @@
package io.github.openflocon.data.local.network.models
import androidx.room.Embedded
data class FloconHttpRequestEntityLite(
val uuid: String,
val deviceId: String, // To associate with a device
// if it's a graphql method, this item is not null
@Embedded(prefix = "graphql_")
val graphql: FloconHttpRequestEntityGraphQlEmbedded?,
@Embedded(prefix = "http_")
val http: FloconHttpRequestEntityHttpEmbedded?,
@Embedded(prefix = "grpc_")
val grpc: FloconHttpRequestEntityGrpcEmbedded?,
val url: String,
val method: String,
val startTime: Long,
val durationMs: Double,
// removed val requestHeaders: Map<String, String>,
// removed val requestBody: String?,
// removed val requestByteSize: Long,
// removed val responseContentType: String?,
// removed val responseBody: String?,
// removed val responseHeaders: Map<String, String>,
// removed val responseByteSize: Long,
)

View file

@ -0,0 +1,17 @@
package io.github.openflocon.data.local.network.models
import androidx.room.Entity
import io.github.openflocon.domain.network.models.NetworkTextFilterColumns
@Entity(
tableName = "network_filter",
primaryKeys = [
"deviceId", "columnName",
],
)
data class NetworkFilterEntity(
val deviceId: String,
val columnName: NetworkTextFilterColumns,
val isEnabled: Boolean,
val itemsAsJson: String,
)