From 7aac7b86dc4d58980313d3d82b0f98016e868c2a Mon Sep 17 00:00:00 2001 From: Florent CHAMPIGNY Date: Tue, 19 Aug 2025 17:33:22 +0200 Subject: [PATCH] feat: [NETWORK] bad connection (#112) * feat: [NETWORK] bad connection * feat: [NETWORK] bad connection * feat: [NETWORK] bad connection * added domain and data / desktop * added domain and data / desktop * added tmp screen * quick save * merged with main * merged with main * quick save * works --------- Co-authored-by: Florent Champigny --- .../plugins/network/FloconNetworkPlugin.kt | 4 +- .../plugins/network/model/BadQualityConfig.kt | 19 + .../io/github/openflocon/flocon/Protocol.kt | 1 + .../network/FloconNetworkPluginImpl.kt | 74 ++- .../network/mapper/BadQualityToJson.kt | 75 +++ .../flocon/okhttp/OkHttpInterceptor.kt | 80 ++- .../flocondesktop/common/db/AppDatabase.kt | 6 +- .../flocondesktop/common/db/RoomModule.kt | 3 + .../network/BadQualityNetworkViewModel.kt | 62 +++ .../features/network/ContentUiState.kt | 2 + .../features/network/NetworkAction.kt | 3 + .../features/network/NetworkUiModule.kt | 2 + .../features/network/NetworkViewModel.kt | 20 +- .../network/mapper/BadQualityMapper.kt | 54 ++ .../badquality/BadQualityConfigUiModel.kt | 41 ++ .../features/network/view/NetworkScreen.kt | 9 + .../badquality/BadNetworkQualityWindow.kt | 463 ++++++++++++++++++ .../network/view/header/NetworkFilter.kt | 17 + .../view/mocks/NetworkEditionWindow.kt | 1 + .../github/openflocon/data/core/network/DI.kt | 2 + .../NetworkQualityLocalDataSource.kt | 24 + .../datasource/NetworkRemoteDataSource.kt | 7 +- .../repository/NetworkRepositoryImpl.kt | 59 ++- .../openflocon/data/local/network/DI.kt | 3 + .../network/dao/NetworkBadQualityConfigDao.kt | 39 ++ .../BadQualityConfigLocalDataSourceImpl.kt | 72 +++ .../local/network/mapper/BadQualityMapper.kt | 73 +++ .../badquality/BadQualityConfigEntity.kt | 20 + .../models/badquality/ErrorEmbedded.kt | 11 + .../datasource/NetworkRemoteDataSourceImpl.kt | 20 + .../data/remote/network/mapper/Mapper.kt | 52 +- .../models/BadQualityConfigDataModel.kt | 25 + .../io/github/openflocon/domain/Protocol.kt | 2 +- .../io/github/openflocon/domain/network/DI.kt | 9 + .../models/BadQualityConfigDomainModel.kt | 20 + .../repository/NetworkBadQualityRepository.kt | 32 ++ .../ObserveNetworkBadQualityUseCase.kt | 27 + .../SaveNetworkBadQualityUseCase.kt | 23 + .../SetupNetworkBadQualityUseCase.kt | 19 + ...UpdateNetworkBadQualityIsEnabledUseCase.kt | 23 + 40 files changed, 1445 insertions(+), 53 deletions(-) create mode 100644 FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/BadQualityConfig.kt create mode 100644 FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/BadQualityNetworkViewModel.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mapper/BadQualityMapper.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/model/badquality/BadQualityConfigUiModel.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/badquality/BadNetworkQualityWindow.kt create mode 100644 FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkQualityLocalDataSource.kt create mode 100644 FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/NetworkBadQualityConfigDao.kt create mode 100644 FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/BadQualityConfigLocalDataSourceImpl.kt create mode 100644 FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/BadQualityMapper.kt create mode 100644 FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/badquality/BadQualityConfigEntity.kt create mode 100644 FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/badquality/ErrorEmbedded.kt create mode 100644 FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/models/BadQualityConfigDataModel.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/BadQualityConfigDomainModel.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkBadQualityRepository.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/ObserveNetworkBadQualityUseCase.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/SaveNetworkBadQualityUseCase.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/SetupNetworkBadQualityUseCase.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/UpdateNetworkBadQualityIsEnabledUseCase.kt diff --git a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt index 184f54c3..cd1cc26f 100644 --- a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt +++ b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.plugins.network import io.github.openflocon.flocon.core.FloconPlugin +import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCall import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallRequest import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallResponse @@ -10,7 +11,8 @@ import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse interface FloconNetworkPlugin : FloconPlugin { val mocks: Collection + val badQualityConfig: BadQualityConfig? fun logRequest(request: FloconNetworkCallRequest) fun logResponse(response: FloconNetworkCallResponse) -} \ No newline at end of file +} diff --git a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/BadQualityConfig.kt b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/BadQualityConfig.kt new file mode 100644 index 00000000..d517050a --- /dev/null +++ b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/BadQualityConfig.kt @@ -0,0 +1,19 @@ +package io.github.openflocon.flocon.plugins.network.model + +data class BadQualityConfig( + val latency: LatencyConfig, + val errorProbability: Double, // chance of triggering an error + val errors: List, // list of errors +) { + class LatencyConfig( + val latencyTriggerProbability: Float, + val minLatencyMs: Long, + val maxLatencyMs: Long, + ) + class Error( + val weight: Float, // increase the probability of being triggered vs all others errors + val errorCode: Int, + val errorBody: String, + val errorContentType: String, // "application/json" + ) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt index 1c71aa9d..7dd2cb35 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt @@ -133,6 +133,7 @@ object Protocol { object Method { const val SetupMocks = "setupMocks" + const val SetupBadNetworkConfig = "setupBadNetworkConfig" } } diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt index f98a4c2a..4d95a2fe 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt @@ -7,8 +7,11 @@ import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.network.mapper.floconNetworkCallRequestToJson import io.github.openflocon.flocon.plugins.network.mapper.floconNetworkCallResponseToJson +import io.github.openflocon.flocon.plugins.network.mapper.parseBadQualityConfig import io.github.openflocon.flocon.plugins.network.mapper.parseMockResponses +import io.github.openflocon.flocon.plugins.network.mapper.toJsonObject import io.github.openflocon.flocon.plugins.network.mapper.writeMockResponsesToJson +import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallRequest import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallResponse import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse @@ -20,6 +23,10 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicReference + +private const val FLOCON_NETWORK_MOCKS_JSON = "flocon_network_mocks.json" +private const val FLOCON_NETWORK_BAD_CONFIG_JSON = "flocon_network_bad_config.json" class FloconNetworkPluginImpl( private val context: Context, @@ -28,6 +35,10 @@ class FloconNetworkPluginImpl( ) : FloconNetworkPlugin { override val mocks = CopyOnWriteArrayList(loadMocksFromFile()) + private val _badQualityConfig = AtomicReference(loadBadNetworkConfig()) + + override val badQualityConfig: BadQualityConfig? + get() = _badQualityConfig.get() override fun logRequest(request: FloconNetworkCallRequest) { sender.send( @@ -59,6 +70,12 @@ class FloconNetworkPluginImpl( mocks.addAll(setup) saveMocksToFile(mocks) } + + Protocol.ToDevice.Network.Method.SetupBadNetworkConfig -> { + val config = parseBadQualityConfig(messageFromServer.body) + _badQualityConfig.set(config) + saveBadNetworkConfig(config) + } } } @@ -68,7 +85,7 @@ class FloconNetworkPluginImpl( private fun saveMocksToFile(mocks: CopyOnWriteArrayList) { try { - val file = File(context.filesDir, "flocon_network_mocks.json") + val file = File(context.filesDir, FLOCON_NETWORK_MOCKS_JSON) val jsonString = writeMockResponsesToJson(mocks).toString(2) FileOutputStream(file).use { it.write(jsonString.toByteArray()) @@ -79,27 +96,8 @@ class FloconNetworkPluginImpl( } private fun loadMocksFromFile(): List { - /* - return listOf( - MockNetworkResponse( - expectation = MockNetworkResponse.Expectation( - method = "*", - urlPattern = ".*todo.*", - pattern = Pattern.compile(".*"), - ), - response = MockNetworkResponse.Response( - httpCode = 201, - mediaType = "application/json", - body = "{ \"florent\" : \"champigny\" }", - delay = 0L, - headers = emptyMap(), - ) - ) - ) - */ - return try { - val file = File(context.filesDir, "flocon_network_mocks.json") + val file = File(context.filesDir, FLOCON_NETWORK_MOCKS_JSON) if (!file.exists()) { return emptyList() } @@ -113,4 +111,38 @@ class FloconNetworkPluginImpl( emptyList() } } + + private fun loadBadNetworkConfig(): BadQualityConfig? { + return try { + val file = File(context.filesDir, FLOCON_NETWORK_BAD_CONFIG_JSON) + if (!file.exists()) { + return null + } + + val jsonString = FileInputStream(file).use { + it.readBytes().toString(Charsets.UTF_8) + } + parseBadQualityConfig(jsonString) + } catch (t: Throwable) { + FloconLogger.logError("issue in loadBadNetworkConfig", t) + null + } + } + + private fun saveBadNetworkConfig(config: BadQualityConfig?) { + try { + val file = File(context.filesDir, FLOCON_NETWORK_BAD_CONFIG_JSON) + if (config == null) { + file.delete() + } else { + val jsonString = toJsonObject(config).toString(2) + FileOutputStream(file).use { + it.write(jsonString.toByteArray()) + } + } + } catch (t: Throwable) { + FloconLogger.logError("issue in saveBadNetworkConfig", t) + } + } + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt new file mode 100644 index 00000000..1eae8582 --- /dev/null +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt @@ -0,0 +1,75 @@ +package io.github.openflocon.flocon.plugins.network.mapper + +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig +import org.json.JSONArray +import org.json.JSONObject + +fun toJsonObject(config: BadQualityConfig): JSONObject { + val jsonObject = JSONObject() + + // Sérialisation de la configuration de latence + val latencyObject = JSONObject() + latencyObject.put("latencyTriggerProbability", config.latency.latencyTriggerProbability) + latencyObject.put("minLatencyMs", config.latency.minLatencyMs) + latencyObject.put("maxLatencyMs", config.latency.maxLatencyMs) + jsonObject.put("latency", latencyObject) + + // Sérialisation de la probabilité d'erreur + jsonObject.put("errorProbability", config.errorProbability) + + // Sérialisation de la liste des erreurs + val errorsArray = JSONArray() + config.errors.forEach { error -> + val errorObject = JSONObject() + errorObject.put("weight", error.weight) + errorObject.put("errorCode", error.errorCode) + errorObject.put("errorBody", error.errorBody) + errorObject.put("errorContentType", error.errorContentType) + errorsArray.put(errorObject) + } + jsonObject.put("errors", errorsArray) + + return jsonObject +} + +fun parseBadQualityConfig(jsonString: String): BadQualityConfig? { + return try { + val jsonObject = JSONObject(jsonString) + + // Parsing de la configuration de latence + val latencyObject = jsonObject.getJSONObject("latency") + val latencyConfig = BadQualityConfig.LatencyConfig( + latencyTriggerProbability = latencyObject.getDouble("latencyTriggerProbability") + .toFloat(), + minLatencyMs = latencyObject.getLong("minLatencyMs"), + maxLatencyMs = latencyObject.getLong("maxLatencyMs") + ) + + // Parsing de la probabilité d'erreur + val errorProbability = jsonObject.getDouble("errorProbability") + + // Parsing de la liste des erreurs + val errorsArray = jsonObject.getJSONArray("errors") + val errorsList = mutableListOf() + for (i in 0 until errorsArray.length()) { + val errorObject = errorsArray.getJSONObject(i) + val error = BadQualityConfig.Error( + weight = errorObject.getDouble("weight").toFloat(), + errorCode = errorObject.getInt("errorCode"), + errorBody = errorObject.getString("errorBody"), + errorContentType = errorObject.getString("errorContentType") + ) + errorsList.add(error) + } + + BadQualityConfig( + latency = latencyConfig, + errorProbability = errorProbability, + errors = errorsList + ) + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "bad connection network parsing issue", t) + null + } +} \ No newline at end of file diff --git a/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt b/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt index 3ff3aca6..628d2f38 100644 --- a/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt +++ b/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.okhttp import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.plugins.network.FloconNetworkPlugin +import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallRequest import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallResponse import io.github.openflocon.flocon.plugins.network.model.FloconNetworkRequest @@ -18,6 +19,7 @@ import okio.Buffer import java.io.IOException import java.nio.charset.StandardCharsets import java.util.UUID +import kotlin.random.Random class FloconOkhttpInterceptor() : Interceptor { @@ -85,7 +87,27 @@ class FloconOkhttpInterceptor() : Interceptor { val response = if(isMocked) { executeMock(request = request, mock = mockConfig) } else { - chain.proceed(request) + val badQualityConfig = floconNetworkPlugin.badQualityConfig // Supposons que cette propriété existe + + if (badQualityConfig != null) { + val latencyProbability = badQualityConfig.latency.latencyTriggerProbability + val shouldSimulateLatency = latencyProbability > 0f && (latencyProbability == 1f || Math.random() < latencyProbability) + if (shouldSimulateLatency) { + try { + val latencyMs = Random.nextLong(badQualityConfig.latency.minLatencyMs, badQualityConfig.latency.maxLatencyMs + 1) + Thread.sleep(latencyMs) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } + } + + failResponseIfNeeded( + badQualityConfig = badQualityConfig, + request = request, + ) ?: chain.proceed(request) // if no need to trigger an error + } else { + chain.proceed(request) + } } val endTime = System.nanoTime() @@ -122,17 +144,6 @@ class FloconOkhttpInterceptor() : Interceptor { grpcStatus = null, ) - /* - floconNetworkPlugin.log( - FloconNetworkCall( - durationMs = durationMs, - floconNetworkType = floconNetworkType, - request = floconNetworkRequest, - response = floconCallResponse, - isMocked = isMocked, - ) - ) - */ floconNetworkPlugin.logResponse( FloconNetworkCallResponse( floconCallId = floconCallId, @@ -149,6 +160,28 @@ class FloconOkhttpInterceptor() : Interceptor { return response } + private fun failResponseIfNeeded( + badQualityConfig: BadQualityConfig, + request: Request, + ): Response? { + val shouldFail = badQualityConfig.errorProbability > 0 && Math.random() < badQualityConfig.errorProbability + if (shouldFail) { + val selectedError = selectRandomError(badQualityConfig.errors) + if (selectedError != null) { + val errorBody = selectedError.errorBody.toResponseBody(selectedError.errorContentType.toMediaTypeOrNull()) + + return Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(selectedError.errorCode) // Utiliser le code d'erreur configuré + .message(getHttpMessage(selectedError.errorCode)) + .body(errorBody) + .build() + } + } + return null + } + private fun findMock( request: Request, floconNetworkPlugin: FloconNetworkPlugin, @@ -197,4 +230,27 @@ class FloconOkhttpInterceptor() : Interceptor { } .build() } +} + +private fun selectRandomError(errors: List): BadQualityConfig.Error? { + if (errors.isEmpty()) { + return null + } + + // Calculer la somme totale des poids + val totalWeight = errors.sumOf { it.weight.toDouble() } + + // Générer un nombre aléatoire entre 0 et la somme totale des poids + var randomNumber = Random.nextDouble(0.0, totalWeight) + + // Parcourir la liste pour trouver l'erreur sélectionnée + for (error in errors) { + randomNumber -= error.weight.toDouble() + if (randomNumber <= 0) { + return error + } + } + + // Cas de secours (ne devrait pas arriver si les poids sont positifs) + return errors.first() } \ No newline at end of file diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt index fec776bb..26b30b65 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt @@ -20,11 +20,13 @@ import io.github.openflocon.data.local.files.dao.FloconFileDao import io.github.openflocon.data.local.files.models.FileEntity import io.github.openflocon.data.local.images.dao.FloconImageDao import io.github.openflocon.data.local.images.models.DeviceImageEntity +import io.github.openflocon.data.local.network.dao.NetworkBadQualityConfigDao import io.github.openflocon.data.local.network.dao.FloconNetworkDao import io.github.openflocon.data.local.network.dao.NetworkFilterDao import io.github.openflocon.data.local.network.dao.NetworkMocksDao import io.github.openflocon.data.local.network.models.FloconNetworkCallEntity import io.github.openflocon.data.local.network.models.NetworkFilterEntity +import io.github.openflocon.data.local.network.models.badquality.BadQualityConfigEntity import io.github.openflocon.data.local.network.models.mock.MockNetworkEntity import io.github.openflocon.data.local.table.dao.FloconTableDao import io.github.openflocon.data.local.table.models.TableEntity @@ -34,7 +36,7 @@ import io.github.openflocon.flocondesktop.common.db.converters.MapStringsConvert import kotlinx.coroutines.Dispatchers @Database( - version = 38, + version = 39, entities = [ FloconNetworkCallEntity::class, FileEntity::class, @@ -50,6 +52,7 @@ import kotlinx.coroutines.Dispatchers NetworkFilterEntity::class, MockNetworkEntity::class, DeviceWithSerialEntity::class, + BadQualityConfigEntity::class, ], ) @TypeConverters( @@ -68,6 +71,7 @@ abstract class AppDatabase : RoomDatabase() { abstract val networkFilterDao: NetworkFilterDao abstract val networkMocksDao: NetworkMocksDao abstract val adbDevicesDao: AdbDevicesDao + abstract val networkBadQualityConfigDao: NetworkBadQualityConfigDao } fun getRoomDatabase(): AppDatabase = getDatabaseBuilder() diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/RoomModule.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/RoomModule.kt index 8af0f500..8ea6cea0 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/RoomModule.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/RoomModule.kt @@ -40,4 +40,7 @@ val roomModule = single { get().adbDevicesDao } + single { + get().networkBadQualityConfigDao + } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/BadQualityNetworkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/BadQualityNetworkViewModel.kt new file mode 100644 index 00000000..fdc0d0e8 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/BadQualityNetworkViewModel.kt @@ -0,0 +1,62 @@ +package io.github.openflocon.flocondesktop.features.network + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.openflocon.domain.common.DispatcherProvider +import io.github.openflocon.domain.feedback.FeedbackDisplayer +import io.github.openflocon.domain.network.usecase.badquality.ObserveNetworkBadQualityUseCase +import io.github.openflocon.domain.network.usecase.badquality.SaveNetworkBadQualityUseCase +import io.github.openflocon.domain.network.usecase.badquality.UpdateNetworkBadQualityIsEnabledUseCase +import io.github.openflocon.flocondesktop.features.network.mapper.toDomain +import io.github.openflocon.flocondesktop.features.network.mapper.toUi +import io.github.openflocon.flocondesktop.features.network.model.badquality.BadQualityConfigUiModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class BadQualityNetworkViewModel( + private val observeNetworkBadQualityUseCase: ObserveNetworkBadQualityUseCase, + private val saveNetworkBadQualityUseCase: SaveNetworkBadQualityUseCase, + private val updateNetworkBadQualityIsEnabledUseCase: UpdateNetworkBadQualityIsEnabledUseCase, + private val dispatcherProvider: DispatcherProvider, + private val feedbackDisplayer: FeedbackDisplayer, +) : ViewModel() { + + enum class Event { + Close + } + + private val _events = Channel() + val events: Flow = _events.receiveAsFlow() + + val viewState = observeNetworkBadQualityUseCase() + .distinctUntilChanged() + .map { toUi(it) } + .flowOn(dispatcherProvider.viewModel) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + fun changeIsEnabled(enabled: Boolean) { + viewModelScope.launch(dispatcherProvider.viewModel) { + updateNetworkBadQualityIsEnabledUseCase(isEnabled = enabled) + } + } + + fun save(uiModel: BadQualityConfigUiModel) { + viewModelScope.launch(dispatcherProvider.viewModel) { + saveNetworkBadQualityUseCase(toDomain(uiModel)) + // close + _events.send(Event.Close) + feedbackDisplayer.displayMessage("Saved") + } + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ContentUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ContentUiState.kt index 8afb8369..b2962ba5 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ContentUiState.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ContentUiState.kt @@ -9,6 +9,7 @@ data class ContentUiState( val selectedRequestId: String?, val detailJsons: Set, val mocksDisplayed: MockDisplayed?, + val badNetworkQualityDisplayed: Boolean, ) @Immutable @@ -21,4 +22,5 @@ fun previewContentUiState() = ContentUiState( selectedRequestId = null, detailJsons = emptySet(), mocksDisplayed = null, + badNetworkQualityDisplayed = false, ) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkAction.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkAction.kt index b275070c..be1f96dd 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkAction.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkAction.kt @@ -22,6 +22,9 @@ sealed interface NetworkAction { data object OpenMocks : NetworkAction data object CloseMocks : NetworkAction + data object OpenBadNetworkQuality : NetworkAction + data object CloseBadNetworkQuality : NetworkAction + data class CloseJsonDetail(val id: String) : NetworkAction data class CopyUrl(val item: NetworkItemViewState) : NetworkAction diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkUiModule.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkUiModule.kt index 177b00e3..e8dc8ec3 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkUiModule.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkUiModule.kt @@ -13,4 +13,6 @@ internal val networkModule = module { factoryOf(::SortAndFilterNetworkItemsProcessor) viewModelOf(::NetworkMocksViewModel) + + viewModelOf(::BadQualityNetworkViewModel) } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkViewModel.kt index 765545fd..2cf9f2b0 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/NetworkViewModel.kt @@ -48,6 +48,7 @@ class NetworkViewModel( selectedRequestId = null, detailJsons = emptySet(), mocksDisplayed = null, + badNetworkQualityDisplayed = false, ), ) @@ -123,7 +124,8 @@ class NetworkViewModel( is NetworkAction.CreateMock -> { openMocks(callId = action.item.uuid) } - + is NetworkAction.OpenBadNetworkQuality -> openBadNetworkQuality() + is NetworkAction.CloseBadNetworkQuality -> closeBadNetworkQuality() is NetworkAction.CloseMocks -> closeMocks() is NetworkAction.CopyCUrl -> onCopyCUrl(action) is NetworkAction.CopyUrl -> onCopyUrl(action) @@ -173,6 +175,22 @@ class NetworkViewModel( } } + private fun openBadNetworkQuality() { + contentState.update { state -> + state.copy( + badNetworkQualityDisplayed = true + ) + } + } + + private fun closeBadNetworkQuality() { + contentState.update { state -> + state.copy( + badNetworkQualityDisplayed = false, + ) + } + } + private fun onClosePanel() { contentState.update { it.copy(selectedRequestId = null) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mapper/BadQualityMapper.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mapper/BadQualityMapper.kt new file mode 100644 index 00000000..3c608648 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mapper/BadQualityMapper.kt @@ -0,0 +1,54 @@ +package io.github.openflocon.flocondesktop.features.network.mapper + +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel +import io.github.openflocon.flocondesktop.features.network.model.badquality.BadQualityConfigUiModel + + +fun toUi(model: BadQualityConfigDomainModel?) = model?.let { + BadQualityConfigUiModel( + isEnabled = it.isEnabled, + latency = toUi(it.latency), + errorProbability = it.errorProbability, + errors = it.errors.map { error -> + toUi(error) + } + ) +} + +private fun toUi(error: BadQualityConfigDomainModel.Error) = BadQualityConfigUiModel.Error( + weight = error.weight, + httpCode = error.httpCode, + body = error.body, + contentType = error.contentType, +) + +private fun toUi(model: BadQualityConfigDomainModel.LatencyConfig) = + BadQualityConfigUiModel.LatencyConfig( + triggerProbability = model.triggerProbability, + minLatencyMs = model.minLatencyMs, + maxLatencyMs = model.maxLatencyMs, + ) + + +fun toDomain(model: BadQualityConfigUiModel) = BadQualityConfigDomainModel( + isEnabled = model.isEnabled, + latency = toDomain(model.latency), + errorProbability = model.errorProbability, + errors = model.errors.map { error -> + toDomain(error) + } +) + +private fun toDomain(error: BadQualityConfigUiModel.Error) = BadQualityConfigDomainModel.Error( + weight = error.weight, + httpCode = error.httpCode, + body = error.body, + contentType = error.contentType, +) + +private fun toDomain(model: BadQualityConfigUiModel.LatencyConfig) = + BadQualityConfigDomainModel.LatencyConfig( + triggerProbability = model.triggerProbability, + minLatencyMs = model.minLatencyMs, + maxLatencyMs = model.maxLatencyMs, + ) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/model/badquality/BadQualityConfigUiModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/model/badquality/BadQualityConfigUiModel.kt new file mode 100644 index 00000000..f654e0b0 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/model/badquality/BadQualityConfigUiModel.kt @@ -0,0 +1,41 @@ +package io.github.openflocon.flocondesktop.features.network.model.badquality + +import java.util.UUID + +data class BadQualityConfigUiModel( + val isEnabled: Boolean, + val latency: LatencyConfig, + val errorProbability: Double, // chance of triggering an error + val errors: List, // list of errors +) { + data class LatencyConfig( + val triggerProbability: Double, + val minLatencyMs: Long, + val maxLatencyMs: Long, + ) + data class Error( + val uuid: String = UUID.randomUUID().toString(), + val weight: Float, // increase the probability of being triggered vs all others errors + val httpCode: Int, + val body: String, + val contentType: String, // "application/json" + ) +} + +fun previewBadQualityConfigUiModel(errorCount: Int) = BadQualityConfigUiModel( + isEnabled = true, + latency = BadQualityConfigUiModel.LatencyConfig( + triggerProbability = 0.1, + minLatencyMs = 100, + maxLatencyMs = 200, + ), + errorProbability = 0.8, + errors = List(errorCount) { + BadQualityConfigUiModel.Error( + weight = 1f, + httpCode = 500, + body = "{\"error\":\"...\"}", + contentType = "application/json" + ) + } +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/NetworkScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/NetworkScreen.kt index 797c4b00..66401576 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/NetworkScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/NetworkScreen.kt @@ -33,6 +33,7 @@ import io.github.openflocon.flocondesktop.features.network.model.NetworkItemView import io.github.openflocon.flocondesktop.features.network.model.previewGraphQlItemViewState import io.github.openflocon.flocondesktop.features.network.model.previewNetworkItemViewState import io.github.openflocon.flocondesktop.features.network.previewNetworkUiState +import io.github.openflocon.flocondesktop.features.network.view.badquality.BadNetworkQualityWindow import io.github.openflocon.flocondesktop.features.network.view.header.NetworkFilter import io.github.openflocon.flocondesktop.features.network.view.header.NetworkItemHeaderView import io.github.openflocon.flocondesktop.features.network.view.mocks.NetworkMocksWindow @@ -185,6 +186,14 @@ fun NetworkScreen( }, ) } + + if(uiState.contentState.badNetworkQualityDisplayed) { + BadNetworkQualityWindow( + onCloseRequest = { + onAction(NetworkAction.CloseBadNetworkQuality) + }, + ) + } } @Composable diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/badquality/BadNetworkQualityWindow.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/badquality/BadNetworkQualityWindow.kt new file mode 100644 index 00000000..012eae19 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/badquality/BadNetworkQualityWindow.kt @@ -0,0 +1,463 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.github.openflocon.flocondesktop.features.network.view.badquality + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.openflocon.flocondesktop.features.network.BadQualityNetworkViewModel +import io.github.openflocon.flocondesktop.features.network.model.badquality.BadQualityConfigUiModel +import io.github.openflocon.flocondesktop.features.network.model.badquality.previewBadQualityConfigUiModel +import io.github.openflocon.flocondesktop.features.network.view.mocks.MockNetworkLabelView +import io.github.openflocon.flocondesktop.features.network.view.mocks.NetworkMockFieldView +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconSurface +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun BadNetworkQualityWindow( + onCloseRequest: () -> Unit, +) { + val viewModel: BadQualityNetworkViewModel = koinViewModel() + val state by viewModel.viewState.collectAsStateWithLifecycle() + + val viewModelEvent by viewModel.events.collectAsStateWithLifecycle(null) + LaunchedEffect(viewModelEvent) { + when (viewModelEvent) { + BadQualityNetworkViewModel.Event.Close -> onCloseRequest() + null -> {} + } + } + + BasicAlertDialog( + onDismissRequest = onCloseRequest, + ) { + FloconSurface { + BadNetworkQualityContent( + state = state, + save = viewModel::save, + close = { onCloseRequest() }, + ) + } + } +} + +@Composable +@Preview +private fun BadNetworkQualityContentPreview() { + FloconTheme { + FloconSurface { + BadNetworkQualityContent( + state = previewBadQualityConfigUiModel( + errorCount = 5 + ), + save = {}, + close = {}, + ) + } + } +} + + +@Composable +fun BadNetworkQualityContent( + state: BadQualityConfigUiModel?, + close: () -> Unit, + save: (state: BadQualityConfigUiModel) -> Unit, + modifier: Modifier = Modifier, +) { + var isEnabled by remember(state) { mutableStateOf(state?.isEnabled ?: true) } + var triggerProbability by remember(state) { + mutableStateOf( + state?.latency?.triggerProbability?.let { it * 100.0 }?.toString() ?: "100" + ) + } + var minLatencyMs by remember(state) { + mutableStateOf( + state?.latency?.minLatencyMs?.toString() ?: "0" + ) + } + var maxLatencyMs by remember(state) { + mutableStateOf( + state?.latency?.maxLatencyMs?.toString() ?: "0" + ) + } + var errorProbability by remember(state) { + mutableStateOf( + state?.errorProbability?.let { it * 100.0 }?.toString() ?: "100" + ) + } + var errors by remember(state) { mutableStateOf(state?.errors ?: emptyList()) } + + var selectedErrorToEdit by remember { mutableStateOf(null) } + Column( + modifier = modifier, + ) { + Box( + Modifier + .fillMaxWidth() + .background(FloconTheme.colorPalette.panel) + .padding(horizontal = 12.dp, vertical = 8.dp), + ) { + Box( + modifier = Modifier.align(Alignment.CenterStart) + .clip(RoundedCornerShape(12.dp)) + .background(FloconTheme.colorPalette.onSurface) + .clickable(onClick = close) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + "Cancel", + style = FloconTheme.typography.titleSmall, + color = FloconTheme.colorPalette.panel, + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .clip(RoundedCornerShape(12.dp)) + .background(FloconTheme.colorPalette.onSurface) + .clickable(onClick = { + val minLatencyMsValue = minLatencyMs.toLong().coerceAtLeast(0) + val maxLatencyMsValue = + maxLatencyMs.toLong().coerceAtLeast(minLatencyMsValue) + save( + BadQualityConfigUiModel( + isEnabled = isEnabled, + latency = BadQualityConfigUiModel.LatencyConfig( + triggerProbability = triggerProbability + .toDoubleOrNull() + ?.let { it / 100.0 } + ?.coerceIn(0.0, 100.0) + ?: 1.0, + minLatencyMs = minLatencyMsValue, + maxLatencyMs = maxLatencyMsValue, + ), + errorProbability = errorProbability + .toDoubleOrNull() + ?.let { it / 100.0 } + ?.coerceIn(0.0, 100.0) + ?: 1.0, + errors = errors, + ) + ) + }) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + "Save", + style = FloconTheme.typography.titleSmall, + color = FloconTheme.colorPalette.panel, + ) + } + } + Column( + modifier = Modifier.padding( + all = 8.dp + ) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + MockNetworkLabelView("isEnabled") + Switch( + modifier = Modifier.scale(0.6f), + checked = isEnabled, + onCheckedChange = { + isEnabled = it + }, + ) + } + NetworkMockFieldView( + label = "triggerProbability 0-100 %", + placeHolder = "0", + value = triggerProbability, + onValueChange = { + if (it.isEmpty() || it.toDoubleOrNull() != null) { + triggerProbability = it + } + } + ) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + NetworkMockFieldView( + modifier = Modifier.weight(1f), + label = "minLatency (ms)", + placeHolder = "0ms", + value = minLatencyMs, + onValueChange = { + if (it.isEmpty() || it.toLongOrNull()?.takeIf { it >= 0L } != null) { + minLatencyMs = it + } + } + ) + NetworkMockFieldView( + modifier = Modifier.weight(1f), + label = "maxLatency (ms)", + placeHolder = "0ms", + value = maxLatencyMs, + onValueChange = { + if (it.isEmpty() || it.toLongOrNull()?.takeIf { it >= 0L } != null) { + maxLatencyMs = it + } + } + ) + } + + NetworkMockFieldView( + label = "errorProbability 0-100 %", + placeHolder = "0", + value = errorProbability, + onValueChange = { + if (it.isEmpty() || it.toDoubleOrNull() != null) { + errorProbability = it + } + } + ) + + ErrorsListView( + modifier = Modifier.padding(top = 16.dp), + errors = errors, + onErrorsClicked = { error -> + selectedErrorToEdit = error + }, + deleteError = { error -> + errors = errors.filterNot { it == error } + } + ) + + selectedErrorToEdit?.let { selectedError -> + BasicAlertDialog( + onDismissRequest = { + selectedErrorToEdit = null + } + ) { + FloconSurface { + ErrorsEditor( + error = selectedError, + onErrorsChange = { error -> + errors = if (errors.any { it.uuid == selectedError.uuid }) { + errors.map { + if (it.uuid == selectedError.uuid) { + error + } else it + } + } else { + errors + error + } + selectedErrorToEdit = null + } + ) + } + } + } + } + } +} + +@Composable +fun ErrorsListView( + errors: List, + onErrorsClicked: (error: BadQualityConfigUiModel.Error) -> Unit, + deleteError: (error: BadQualityConfigUiModel.Error) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text("Errors", style = FloconTheme.typography.titleMedium) + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(FloconTheme.colorPalette.onSurface) + .clickable { + onErrorsClicked( + BadQualityConfigUiModel.Error( + // new error + weight = 1f, + httpCode = 500, + body = "", + contentType = "application/json" + ) + ) + } + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + "Add", + style = FloconTheme.typography.titleSmall, + color = FloconTheme.colorPalette.panel, + ) + } + } + LazyRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(errors) { error -> + Column( + modifier = Modifier + .width(230.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFFEFEFEF).copy(alpha = 0.2f)) + .clickable { + onErrorsClicked(error) + } + .padding(8.dp) + ) { + + val textStyle = FloconTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Thin, + color = FloconTheme.colorPalette.onSurface, + ) + + Text("Weight : ${error.weight}", style = textStyle) + Text("HttpCode : ${error.httpCode}", style = textStyle) // or throwable ? + Text(error.contentType, style = textStyle) + Text("Body : ${error.body}", maxLines = 2, style = textStyle) + + // bouton supprimer + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Image( + imageVector = Icons.Default.Delete, + contentDescription = "Delete error", + modifier = Modifier + .size(24.dp) + .clickable { + deleteError( + error + ) + }, + colorFilter = ColorFilter.tint(FloconTheme.colorPalette.onSurface) + ) + } + } + } + } + + } +} + + +@Composable +fun ErrorsEditor( + error: BadQualityConfigUiModel.Error, + onErrorsChange: (BadQualityConfigUiModel.Error) -> Unit, +) { + var weight by remember(error) { mutableStateOf(error.weight.toString()) } + var httpCode by remember(error) { mutableStateOf(error.httpCode.toString()) } + var contentType by remember() { mutableStateOf(error.contentType) } + var body by remember(error) { mutableStateOf(error.body) } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + + // bouton ajouter + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(FloconTheme.colorPalette.onSurface) + .clickable { + onErrorsChange( + error.copy( + weight = weight.toFloatOrNull() ?: error.weight, + httpCode = httpCode.toIntOrNull() ?: error.httpCode, + contentType = contentType, + body = body + ) + ) + } + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + "Save", + style = FloconTheme.typography.titleSmall, + color = FloconTheme.colorPalette.panel, + ) + } + Column( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFFEFEFEF).copy(alpha = 0.2f)) + .padding(8.dp) + ) { + NetworkMockFieldView( + label = "Weight", + placeHolder = "eg: 1.0", + value = weight, + onValueChange = { + if (it.isEmpty() || it.toFloatOrNull() != null) { + weight = it + } + } + ) + + NetworkMockFieldView( + label = "HTTP Code", + placeHolder = "eg: 500", + value = httpCode, + onValueChange = { + if (it.isEmpty() || it.toIntOrNull() != null) { + httpCode = it + } + } + ) + + NetworkMockFieldView( + label = "Content-Type", + placeHolder = "application/json", + value = contentType, + onValueChange = { + contentType = it + } + ) + + NetworkMockFieldView( + label = "Body", + placeHolder = "{\"error\":\"...\"}", + value = body, + onValueChange = { + body = it + } + ) + } + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/header/NetworkFilter.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/header/NetworkFilter.kt index 072069c2..495a9a2e 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/header/NetworkFilter.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/header/NetworkFilter.kt @@ -60,6 +60,23 @@ fun NetworkFilter( ), ) } + Box( + modifier = Modifier.clip(RoundedCornerShape(8.dp)) + .background(Color.White.copy(alpha = 0.9f)) + .clickable( + onClick = { + onAction(NetworkAction.OpenBadNetworkQuality) + }, + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = "Bad Network Quality", + style = FloconTheme.typography.bodyMedium.copy( + color = Color.Black, + ), + ) + } } } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/mocks/NetworkEditionWindow.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/mocks/NetworkEditionWindow.kt index 44c1e7f1..9f8294a0 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/mocks/NetworkEditionWindow.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/view/mocks/NetworkEditionWindow.kt @@ -46,6 +46,7 @@ import io.github.openflocon.flocondesktop.features.network.model.mocks.MockNetwo import io.github.openflocon.flocondesktop.features.network.model.mocks.SelectedMockUiModel import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.library.designsystem.components.FloconSurface +import org.jetbrains.compose.ui.tooling.preview.Preview import kotlin.collections.plus @Composable diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/DI.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/DI.kt index 13e82483..287d8b4d 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/DI.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/DI.kt @@ -3,6 +3,7 @@ package io.github.openflocon.data.core.network import io.github.openflocon.data.core.network.repository.NetworkFilterRepositoryImpl import io.github.openflocon.data.core.network.repository.NetworkRepositoryImpl import io.github.openflocon.domain.messages.repository.MessagesReceiverRepository +import io.github.openflocon.domain.network.repository.NetworkBadQualityRepository import io.github.openflocon.domain.network.repository.NetworkFilterRepository import io.github.openflocon.domain.network.repository.NetworkMocksRepository import io.github.openflocon.domain.network.repository.NetworkRepository @@ -16,6 +17,7 @@ internal val networkModule = module { singleOf(::NetworkRepositoryImpl) { bind() bind() + bind() bind() } } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkQualityLocalDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkQualityLocalDataSource.kt new file mode 100644 index 00000000..0e1a1547 --- /dev/null +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkQualityLocalDataSource.kt @@ -0,0 +1,24 @@ +package io.github.openflocon.data.core.network.datasource + +import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel +import kotlinx.coroutines.flow.Flow + +interface NetworkQualityLocalDataSource { + suspend fun save( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + config: BadQualityConfigDomainModel + ) + + suspend fun getNetworkQuality(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel) : BadQualityConfigDomainModel? + + fun observe( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + ): Flow + + suspend fun updateIsEnabled( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + isEnabled: Boolean, + ) + +} diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkRemoteDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkRemoteDataSource.kt index eb496c8e..9ff74e23 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkRemoteDataSource.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkRemoteDataSource.kt @@ -2,6 +2,7 @@ package io.github.openflocon.data.core.network.datasource import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallIdDomainModel import io.github.openflocon.domain.network.models.FloconNetworkResponseDomainModel @@ -15,10 +16,14 @@ interface NetworkRemoteDataSource { mocks: List, ) + suspend fun setupBadNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + config: BadQualityConfigDomainModel? + ) + fun getRequestData(message: FloconIncomingMessageDomainModel): FloconNetworkCallDomainModel? fun getCallId(message: FloconIncomingMessageDomainModel): FloconNetworkCallIdDomainModel? fun getResponseData(message: FloconIncomingMessageDomainModel): FloconNetworkResponseOnlyDomainModel? - } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt index 11b3f0f5..377985ff 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt @@ -2,16 +2,19 @@ package io.github.openflocon.data.core.network.repository import io.github.openflocon.data.core.network.datasource.NetworkLocalDataSource import io.github.openflocon.data.core.network.datasource.NetworkMocksLocalDataSource +import io.github.openflocon.data.core.network.datasource.NetworkQualityLocalDataSource import io.github.openflocon.data.core.network.datasource.NetworkRemoteDataSource import io.github.openflocon.domain.Protocol import io.github.openflocon.domain.common.DispatcherProvider import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel import io.github.openflocon.domain.messages.repository.MessagesReceiverRepository +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.domain.network.models.FloconNetworkResponseDomainModel import io.github.openflocon.domain.network.models.FloconNetworkResponseOnlyDomainModel import io.github.openflocon.domain.network.models.MockNetworkDomainModel +import io.github.openflocon.domain.network.repository.NetworkBadQualityRepository import io.github.openflocon.domain.network.repository.NetworkImageRepository import io.github.openflocon.domain.network.repository.NetworkMocksRepository import io.github.openflocon.domain.network.repository.NetworkRepository @@ -23,11 +26,13 @@ class NetworkRepositoryImpl( private val dispatcherProvider: DispatcherProvider, private val networkLocalDataSource: NetworkLocalDataSource, private val networkMocksLocalDataSource: NetworkMocksLocalDataSource, + private val networkQualityLocalDataSource: NetworkQualityLocalDataSource, private val networkImageRepository: NetworkImageRepository, private val networkRemoteDataSource: NetworkRemoteDataSource, ) : NetworkRepository, NetworkMocksRepository, - MessagesReceiverRepository { + MessagesReceiverRepository, + NetworkBadQualityRepository { override val pluginName = listOf(Protocol.FromDevice.Network.Plugin) @@ -266,4 +271,56 @@ class NetworkRepositoryImpl( ) } + override suspend fun setupBadNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + config: BadQualityConfigDomainModel? + ) { + withContext(dispatcherProvider.data) { + networkRemoteDataSource.setupBadNetworkQuality( + deviceIdAndPackageName = deviceIdAndPackageName, + config = config, + ) + } + } + + override suspend fun saveBadNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + config: BadQualityConfigDomainModel + ) { + withContext(dispatcherProvider.data) { + networkQualityLocalDataSource.save( + deviceIdAndPackageName = deviceIdAndPackageName, + config = config, + ) + } + } + + override suspend fun getNetworkQuality(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): BadQualityConfigDomainModel? { + return withContext(dispatcherProvider.data) { + networkQualityLocalDataSource.getNetworkQuality( + deviceIdAndPackageName = deviceIdAndPackageName, + ) + } + } + + override fun observeNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + ): Flow { + return networkQualityLocalDataSource.observe( + deviceIdAndPackageName = deviceIdAndPackageName, + ) + } + + override suspend fun setNetworkQualityIsEnabled( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + isEnabled: Boolean, + ) { + withContext(dispatcherProvider.data) { + networkQualityLocalDataSource.updateIsEnabled( + deviceIdAndPackageName = deviceIdAndPackageName, + isEnabled = isEnabled, + ) + } + } + } diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/DI.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/DI.kt index 884acfe2..448e028c 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/DI.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/DI.kt @@ -3,6 +3,8 @@ 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.core.network.datasource.NetworkMocksLocalDataSource +import io.github.openflocon.data.core.network.datasource.NetworkQualityLocalDataSource +import io.github.openflocon.data.local.network.datasource.BadQualityConfigLocalDataSourceImpl import io.github.openflocon.data.local.network.datasource.NetworkFilterLocalDataSourceRoom import io.github.openflocon.data.local.network.datasource.NetworkLocalDataSourceRoom import io.github.openflocon.data.local.network.datasource.NetworkMocksLocalDataSourceImpl @@ -14,4 +16,5 @@ internal val networkModule = module { singleOf(::NetworkLocalDataSourceRoom) bind NetworkLocalDataSource::class singleOf(::NetworkFilterLocalDataSourceRoom) bind NetworkFilterLocalDataSource::class singleOf(::NetworkMocksLocalDataSourceImpl) bind NetworkMocksLocalDataSource::class + singleOf(::BadQualityConfigLocalDataSourceImpl) bind NetworkQualityLocalDataSource::class } diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/NetworkBadQualityConfigDao.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/NetworkBadQualityConfigDao.kt new file mode 100644 index 00000000..b52e1989 --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/NetworkBadQualityConfigDao.kt @@ -0,0 +1,39 @@ +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.badquality.BadQualityConfigEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface NetworkBadQualityConfigDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(config: BadQualityConfigEntity) + + @Query(""" + SELECT * + FROM BadQualityConfigEntity + WHERE deviceId = :deviceId AND packageName = :packageName + LIMIT 1 + """) + suspend fun get(deviceId: String, packageName: String): BadQualityConfigEntity? + + + @Query(""" + SELECT * + FROM BadQualityConfigEntity + WHERE deviceId = :deviceId AND packageName = :packageName + LIMIT 1 + """) + fun observe(deviceId: String, packageName: String): Flow + + @Query(""" + UPDATE BadQualityConfigEntity + SET isEnabled = :isEnabled + WHERE deviceId = :deviceId + AND packageName = :packageName + """) + suspend fun updateIsEnabled(deviceId: String, packageName: String, isEnabled: Boolean) +} diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/BadQualityConfigLocalDataSourceImpl.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/BadQualityConfigLocalDataSourceImpl.kt new file mode 100644 index 00000000..a33066a6 --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/BadQualityConfigLocalDataSourceImpl.kt @@ -0,0 +1,72 @@ +package io.github.openflocon.data.local.network.datasource + +import io.github.openflocon.data.core.network.datasource.NetworkQualityLocalDataSource +import io.github.openflocon.data.local.network.dao.NetworkBadQualityConfigDao +import io.github.openflocon.data.local.network.mapper.toDomain +import io.github.openflocon.data.local.network.mapper.toEntity +import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.serialization.json.Json + +class BadQualityConfigLocalDataSourceImpl( + private val networkBadQualityConfigDao: NetworkBadQualityConfigDao, + private val json: Json, +) : NetworkQualityLocalDataSource { + + override suspend fun save( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + config: BadQualityConfigDomainModel + ) { + networkBadQualityConfigDao.save( + toEntity( + json = json, + config = config, + deviceIdAndPackageName = deviceIdAndPackageName + ) + ) + } + + override suspend fun getNetworkQuality(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): BadQualityConfigDomainModel? { + return networkBadQualityConfigDao.get( + deviceId = deviceIdAndPackageName.deviceId, + packageName = deviceIdAndPackageName.packageName + )?.let { + toDomain( + json = json, + entity = it + ) + } + } + + override fun observe( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel + ): Flow { + return networkBadQualityConfigDao.observe( + deviceId = deviceIdAndPackageName.deviceId, + packageName = deviceIdAndPackageName.packageName + ).map { + it?.let { + toDomain( + json = json, + entity = it + ) + } + }.distinctUntilChanged() + } + + override suspend fun updateIsEnabled( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + isEnabled: Boolean, + ) { + networkBadQualityConfigDao.updateIsEnabled( + deviceId = deviceIdAndPackageName.deviceId, + packageName = deviceIdAndPackageName.packageName, + isEnabled = isEnabled + ) + } +} + + diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/BadQualityMapper.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/BadQualityMapper.kt new file mode 100644 index 00000000..a04eca6f --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/BadQualityMapper.kt @@ -0,0 +1,73 @@ +package io.github.openflocon.data.local.network.mapper + +import io.github.openflocon.data.local.network.models.badquality.BadQualityConfigEntity +import io.github.openflocon.data.local.network.models.badquality.ErrorEmbedded +import io.github.openflocon.data.local.network.models.badquality.LatencyConfigEmbedded +import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel +import kotlinx.serialization.json.Json + +fun toDomain(json: Json, entity: BadQualityConfigEntity): BadQualityConfigDomainModel { + val errors = try { + json.decodeFromString>(entity.errors) + .map { toDomain(it) } + } catch (t: Throwable) { + t.printStackTrace() + emptyList() + } + return BadQualityConfigDomainModel( + isEnabled = entity.isEnabled, + latency = BadQualityConfigDomainModel.LatencyConfig( + triggerProbability = entity.latency.triggerProbability, + minLatencyMs = entity.latency.minLatencyMs, + maxLatencyMs = entity.latency.maxLatencyMs, + ), + errorProbability = entity.errorProbability, + errors = errors, + ) +} + +fun toDomain(error: ErrorEmbedded): BadQualityConfigDomainModel.Error { + return BadQualityConfigDomainModel.Error( + weight = error.weight, + httpCode = error.httpCode, + body = error.body, + contentType = error.contentType, + ) +} + + +fun toEntity( + json: Json, + config: BadQualityConfigDomainModel, + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel +): BadQualityConfigEntity { + val errorsEmbedded = config.errors.map { + toEntity(it) + } + val errors = try { + json.encodeToString>(errorsEmbedded) + } catch (t: Throwable) { + t.printStackTrace() + "[]" + } + return BadQualityConfigEntity( + deviceId = deviceIdAndPackageName.deviceId, + packageName = deviceIdAndPackageName.packageName, + isEnabled = config.isEnabled, + latency = LatencyConfigEmbedded( + triggerProbability = config.latency.triggerProbability, + minLatencyMs = config.latency.minLatencyMs, + maxLatencyMs = config.latency.maxLatencyMs, + ), + errorProbability = config.errorProbability, + errors = errors, + ) +} + +private fun toEntity(error: BadQualityConfigDomainModel.Error): ErrorEmbedded = ErrorEmbedded( + weight = error.weight, + httpCode = error.httpCode, + body = error.body, + contentType = error.contentType, +) diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/badquality/BadQualityConfigEntity.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/badquality/BadQualityConfigEntity.kt new file mode 100644 index 00000000..83b9e938 --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/badquality/BadQualityConfigEntity.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.data.local.network.models.badquality + +import androidx.room.Embedded +import androidx.room.Entity + +@Entity(primaryKeys = ["deviceId", "packageName"]) +data class BadQualityConfigEntity( + val deviceId: String, + val packageName: String, + val isEnabled: Boolean, + @Embedded val latency: LatencyConfigEmbedded, + val errorProbability: Double, + val errors: String, // saved as json +) + +data class LatencyConfigEmbedded( + val triggerProbability: Double, + val minLatencyMs: Long, + val maxLatencyMs: Long, +) diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/badquality/ErrorEmbedded.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/badquality/ErrorEmbedded.kt new file mode 100644 index 00000000..59131ee3 --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/badquality/ErrorEmbedded.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.data.local.network.models.badquality + +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorEmbedded( + val weight: Float, + val httpCode: Int, + val body: String, + val contentType: String, +) diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/datasource/NetworkRemoteDataSourceImpl.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/datasource/NetworkRemoteDataSourceImpl.kt index ccd42df9..f5bfe907 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/datasource/NetworkRemoteDataSourceImpl.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/datasource/NetworkRemoteDataSourceImpl.kt @@ -4,6 +4,7 @@ import com.flocon.data.remote.common.safeDecodeFromString import com.flocon.data.remote.models.FloconOutgoingMessageDataModel import com.flocon.data.remote.models.toRemote import com.flocon.data.remote.network.mapper.listToRemote +import com.flocon.data.remote.network.mapper.toRemote import com.flocon.data.remote.network.models.FloconNetworkCallIdDataModel import com.flocon.data.remote.network.models.FloconNetworkRequestDataModel import com.flocon.data.remote.network.models.FloconNetworkResponseDataModel @@ -13,6 +14,7 @@ import io.github.openflocon.data.core.network.datasource.NetworkRemoteDataSource import io.github.openflocon.domain.Protocol import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallIdDomainModel import io.github.openflocon.domain.network.models.FloconNetworkResponseDomainModel @@ -41,6 +43,24 @@ class NetworkRemoteDataSourceImpl( ) } + override suspend fun setupBadNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + config: BadQualityConfigDomainModel?, + ) { + server.sendMessageToClient( + deviceIdAndPackageName = deviceIdAndPackageName.toRemote(), + message = FloconOutgoingMessageDataModel( + plugin = Protocol.ToDevice.Network.Plugin, + method = Protocol.ToDevice.Network.Method.SetupBadNetworkConfig, + body = if(config == null || config.isEnabled.not()) { + "{}" // empty json to clear the config mobile side + } else { + Json.Default.encodeToString(toRemote(config)) + }, + ), + ) + } + override fun getRequestData(message: FloconIncomingMessageDomainModel): FloconNetworkCallDomainModel? { return json.safeDecodeFromString(message.body) ?.let { com.flocon.data.remote.network.mapper.toDomain(it) } diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/Mapper.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/Mapper.kt index 5454c802..ffe702ee 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/Mapper.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/Mapper.kt @@ -1,31 +1,35 @@ package com.flocon.data.remote.network.mapper +import com.flocon.data.remote.network.models.BadQualityConfigDataModel import com.flocon.data.remote.network.models.FloconNetworkRequestDataModel import com.flocon.data.remote.network.models.MockNetworkResponseDataModel import io.github.openflocon.data.core.network.graphql.model.GraphQlExtracted import io.github.openflocon.data.core.network.graphql.model.GraphQlRequestBody import io.github.openflocon.data.core.network.graphql.model.GraphQlResponseBody +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.domain.network.models.FloconNetworkRequestDomainModel import io.github.openflocon.domain.network.models.MockNetworkDomainModel import kotlinx.serialization.json.Json import kotlin.uuid.ExperimentalUuidApi -fun listToRemote(mocks: List): List = mocks.map { toRemote(it) } +fun listToRemote(mocks: List): List = + mocks.map { toRemote(it) } -fun toRemote(mock: MockNetworkDomainModel): MockNetworkResponseDataModel = MockNetworkResponseDataModel( - expectation = MockNetworkResponseDataModel.Expectation( - urlPattern = mock.expectation.urlPattern, - method = mock.expectation.method, - ), - response = MockNetworkResponseDataModel.Response( - httpCode = mock.response.httpCode, - body = mock.response.body, - mediaType = mock.response.mediaType, - delay = mock.response.delay, - headers = mock.response.headers, - ), -) +fun toRemote(mock: MockNetworkDomainModel): MockNetworkResponseDataModel = + MockNetworkResponseDataModel( + expectation = MockNetworkResponseDataModel.Expectation( + urlPattern = mock.expectation.urlPattern, + method = mock.expectation.method, + ), + response = MockNetworkResponseDataModel.Response( + httpCode = mock.response.httpCode, + body = mock.response.body, + mediaType = mock.response.mediaType, + delay = mock.response.delay, + headers = mock.response.headers, + ), + ) @OptIn(ExperimentalUuidApi::class) fun toDomain(decoded: FloconNetworkRequestDataModel): FloconNetworkCallDomainModel? = try { @@ -122,3 +126,23 @@ fun computeIsGraphQlSuccess( if (responseHttpCode !in 200..299) return false return response?.errors?.takeUnless { it.isEmpty() } == null } + +fun toRemote(domain: BadQualityConfigDomainModel): BadQualityConfigDataModel = + BadQualityConfigDataModel( + latency = with(domain.latency) { + BadQualityConfigDataModel.LatencyConfig( + latencyTriggerProbability = triggerProbability, + minLatencyMs = minLatencyMs, + maxLatencyMs = maxLatencyMs, + ) + }, + errorProbability = domain.errorProbability, + errors = domain.errors.map { + BadQualityConfigDataModel.Error( + weight = it.weight, + errorCode = it.httpCode, + errorBody = it.body, + errorContentType = it.contentType, + ) + }, + ) diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/models/BadQualityConfigDataModel.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/models/BadQualityConfigDataModel.kt new file mode 100644 index 00000000..cd1c811e --- /dev/null +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/models/BadQualityConfigDataModel.kt @@ -0,0 +1,25 @@ +package com.flocon.data.remote.network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class BadQualityConfigDataModel( + val latency: LatencyConfig, + val errorProbability: Double, // chance of triggering an error + val errors: List, // list of errors +) { + @Serializable + data class LatencyConfig( + val latencyTriggerProbability: Double, + val minLatencyMs: Long, + val maxLatencyMs: Long, + ) + + @Serializable + data class Error( + val weight: Float, // increase the probability of being triggered vs all others errors + val errorCode: Int, + val errorBody: String, + val errorContentType: String, // "application/json" + ) +} diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt index e5faf0b9..8956bc00 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt @@ -65,7 +65,6 @@ object Protocol { const val Plugin = "network" object Method { - const val LogNetworkCall = "logNetworkCall" const val LogNetworkCallRequest = "logNetworkCallRequest" const val LogNetworkCallResponse = "logNetworkCallResponse" } @@ -133,6 +132,7 @@ object Protocol { object Method { const val SetupMocks = "setupMocks" + const val SetupBadNetworkConfig = "setupBadNetworkConfig" } } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/DI.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/DI.kt index 25858c1d..f955ff93 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/DI.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/DI.kt @@ -9,6 +9,10 @@ import io.github.openflocon.domain.network.usecase.RemoveHttpRequestUseCase import io.github.openflocon.domain.network.usecase.RemoveHttpRequestsBeforeUseCase import io.github.openflocon.domain.network.usecase.ResetCurrentDeviceHttpRequestsUseCase import io.github.openflocon.domain.network.usecase.UpdateNetworkFilterUseCase +import io.github.openflocon.domain.network.usecase.badquality.ObserveNetworkBadQualityUseCase +import io.github.openflocon.domain.network.usecase.badquality.SaveNetworkBadQualityUseCase +import io.github.openflocon.domain.network.usecase.badquality.SetupNetworkBadQualityUseCase +import io.github.openflocon.domain.network.usecase.badquality.UpdateNetworkBadQualityIsEnabledUseCase import io.github.openflocon.domain.network.usecase.mocks.AddNetworkMocksUseCase import io.github.openflocon.domain.network.usecase.mocks.DeleteNetworkMocksUseCase import io.github.openflocon.domain.network.usecase.mocks.GenerateNetworkMockFromNetworkCallUseCase @@ -38,4 +42,9 @@ internal val networkModule = module { factoryOf(::GetNetworkMockByIdUseCase) factoryOf(::GenerateNetworkMockFromNetworkCallUseCase) factoryOf(::UpdateNetworkMockIsEnabledUseCase) + // bad quality + factoryOf(::ObserveNetworkBadQualityUseCase) + factoryOf(::SaveNetworkBadQualityUseCase) + factoryOf(::SetupNetworkBadQualityUseCase) + factoryOf(::UpdateNetworkBadQualityIsEnabledUseCase) } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/BadQualityConfigDomainModel.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/BadQualityConfigDomainModel.kt new file mode 100644 index 00000000..0ac66ab2 --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/BadQualityConfigDomainModel.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.domain.network.models + +data class BadQualityConfigDomainModel( + val isEnabled: Boolean, + val latency: LatencyConfig, + val errorProbability: Double, // chance of triggering an error + val errors: List, // list of errors +) { + data class LatencyConfig( + val triggerProbability: Double, + val minLatencyMs: Long, + val maxLatencyMs: Long, + ) + data class Error( + val weight: Float, // increase the probability of being triggered vs all others errors + val httpCode: Int, + val body: String, + val contentType: String, // "application/json" + ) +} diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkBadQualityRepository.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkBadQualityRepository.kt new file mode 100644 index 00000000..6d570e39 --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkBadQualityRepository.kt @@ -0,0 +1,32 @@ +package io.github.openflocon.domain.network.repository + +import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel +import kotlinx.coroutines.flow.Flow + +interface NetworkBadQualityRepository { + // send to device + suspend fun setupBadNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + config: BadQualityConfigDomainModel? + ) + + // save locally + suspend fun saveBadNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + config: BadQualityConfigDomainModel, + ) + + suspend fun getNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + ) : BadQualityConfigDomainModel? + + fun observeNetworkQuality( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + ) : Flow + + suspend fun setNetworkQualityIsEnabled( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + isEnabled: Boolean, + ) +} diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/ObserveNetworkBadQualityUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/ObserveNetworkBadQualityUseCase.kt new file mode 100644 index 00000000..71aac617 --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/ObserveNetworkBadQualityUseCase.kt @@ -0,0 +1,27 @@ +package io.github.openflocon.domain.network.usecase.badquality + +import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdAndPackageNameUseCase +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel +import io.github.openflocon.domain.network.models.MockNetworkDomainModel +import io.github.openflocon.domain.network.repository.NetworkBadQualityRepository +import io.github.openflocon.domain.network.repository.NetworkMocksRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +class ObserveNetworkBadQualityUseCase( + private val networkBadQualityRepository: NetworkBadQualityRepository, + private val observeCurrentDeviceIdAndPackageNameUseCase: ObserveCurrentDeviceIdAndPackageNameUseCase, +) { + operator fun invoke(): Flow = + observeCurrentDeviceIdAndPackageNameUseCase() + .flatMapLatest { current -> + if (current == null) { + flowOf(null) + } else { + networkBadQualityRepository.observeNetworkQuality(deviceIdAndPackageName = current) + } + } + .distinctUntilChanged() +} diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/SaveNetworkBadQualityUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/SaveNetworkBadQualityUseCase.kt new file mode 100644 index 00000000..bceaad4e --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/SaveNetworkBadQualityUseCase.kt @@ -0,0 +1,23 @@ +package io.github.openflocon.domain.network.usecase.badquality + +import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel +import io.github.openflocon.domain.network.repository.NetworkBadQualityRepository +import io.github.openflocon.domain.network.repository.NetworkMocksRepository + +class SaveNetworkBadQualityUseCase( + private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase, + private val networkBadQualityRepository: NetworkBadQualityRepository, + private val setupNetworkBadQualityUseCase: SetupNetworkBadQualityUseCase, +) { + suspend operator fun invoke(config: BadQualityConfigDomainModel) { + getCurrentDeviceIdAndPackageNameUseCase()?.let { deviceIdAndPackageName -> + networkBadQualityRepository.saveBadNetworkQuality( + config = config, + deviceIdAndPackageName = deviceIdAndPackageName, + ) + // then send to device + setupNetworkBadQualityUseCase() + } + } +} diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/SetupNetworkBadQualityUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/SetupNetworkBadQualityUseCase.kt new file mode 100644 index 00000000..690ccd99 --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/SetupNetworkBadQualityUseCase.kt @@ -0,0 +1,19 @@ +package io.github.openflocon.domain.network.usecase.badquality + +import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase +import io.github.openflocon.domain.network.repository.NetworkBadQualityRepository +import io.github.openflocon.domain.network.repository.NetworkMocksRepository + +class SetupNetworkBadQualityUseCase( + private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase, + private val networkBadQualityRepository: NetworkBadQualityRepository, +) { + suspend operator fun invoke() { + getCurrentDeviceIdAndPackageNameUseCase()?.let { deviceIdAndPackageName -> + networkBadQualityRepository.setupBadNetworkQuality( + config = networkBadQualityRepository.getNetworkQuality(deviceIdAndPackageName), + deviceIdAndPackageName = deviceIdAndPackageName, + ) + } + } +} diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/UpdateNetworkBadQualityIsEnabledUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/UpdateNetworkBadQualityIsEnabledUseCase.kt new file mode 100644 index 00000000..294b4d2f --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/badquality/UpdateNetworkBadQualityIsEnabledUseCase.kt @@ -0,0 +1,23 @@ +package io.github.openflocon.domain.network.usecase.badquality + +import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase +import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel +import io.github.openflocon.domain.network.repository.NetworkBadQualityRepository +import io.github.openflocon.domain.network.repository.NetworkMocksRepository + +class UpdateNetworkBadQualityIsEnabledUseCase( + private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase, + private val networkBadQualityRepository: NetworkBadQualityRepository, + private val setupNetworkBadQualityUseCase: SetupNetworkBadQualityUseCase, +) { + suspend operator fun invoke(isEnabled: Boolean) { + getCurrentDeviceIdAndPackageNameUseCase()?.let { deviceIdAndPackageName -> + networkBadQualityRepository.setNetworkQualityIsEnabled( + isEnabled = isEnabled, + deviceIdAndPackageName = deviceIdAndPackageName, + ) + // then send to device + setupNetworkBadQualityUseCase() + } + } +}