From 2e5ff5737054fc76bde0fd67451cb8b0f11338b8 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Sat, 7 Mar 2026 21:16:03 +0100 Subject: [PATCH 01/38] feat: Start rework --- .../deeplinks-no-op/build.gradle.kts | 118 ++++++++++++ .../plugins/deeplinks/FloconDeeplinksNoOp.kt | 35 ++++ FloconAndroid/deeplinks/build.gradle.kts | 121 ++++++++++++ .../plugins/deeplinks/FloconDeeplinks.kt | 68 +++++++ .../flocon/plugins/deeplinks/Mapping.kt | 25 +++ .../plugins/deeplinks/model/DeeplinkModel.kt | 13 ++ .../deeplinks/model/DeeplinksRemote.kt | 22 +++ .../io/github/openflocon/flocon/FloconApp.kt | 27 ++- .../openflocon/flocon/FloconConfiguration.kt | 27 +++ .../github/openflocon/flocon/FloconPlugin.kt | 37 ++++ .../analytics/FloconAnalyticsPlugin.kt | 19 +- .../FloconCrashReporterPlugin.kt | 15 +- .../dashboard/FloconDashboardPlugin.kt | 10 +- .../plugins/database/FloconDatabasePlugin.kt | 12 +- .../deeplinks/FloconDeeplinksPlugin.kt | 2 +- .../plugins/device/FloconDevicePlugin.kt | 11 +- .../flocon/plugins/files/FloconFilesPlugin.kt | 14 +- .../plugins/network/FloconNetworkPlugin.kt | 15 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 20 +- .../plugins/tables/FloconTablesPlugin.kt | 20 +- FloconAndroid/flocon/build.gradle.kts | 1 + .../io/github/openflocon/flocon/Flocon.kt | 22 +-- .../io/github/openflocon/flocon/FloconCore.kt | 10 +- .../flocon/client/FloconClientImpl.kt | 171 +++++++---------- .../openflocon/flocon/core/FloconEncoder.kt | 2 +- .../flocon/core/FloconMessageSender.kt | 2 +- .../analytics/FloconAnalyticsPlugin.kt | 19 +- .../FloconCrashReporterPlugin.kt | 31 +++- .../dashboard/FloconDashboardPlugin.kt | 30 +-- .../plugins/database/FloconDatabasePlugin.kt | 24 ++- .../deeplinks/FloconDeeplinksPlugin.kt | 29 ++- .../plugins/device/FloconDevicePluginImpl.kt | 23 ++- .../flocon/plugins/files/FloconFilesPlugin.kt | 36 ++-- .../network/FloconNetworkPluginImpl.kt | 55 +++--- .../sharedprefs/FloconSharedPrefsPlugin.kt | 174 +++++------------- .../plugins/tables/FloconTablesPlugin.kt | 27 ++- .../myapplication/multi/MainActivity.kt | 8 +- FloconAndroid/settings.gradle.kts | 2 + 38 files changed, 909 insertions(+), 388 deletions(-) create mode 100644 FloconAndroid/deeplinks-no-op/build.gradle.kts create mode 100644 FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt create mode 100644 FloconAndroid/deeplinks/build.gradle.kts create mode 100644 FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt create mode 100644 FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt create mode 100644 FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt create mode 100644 FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt create mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt create mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt diff --git a/FloconAndroid/deeplinks-no-op/build.gradle.kts b/FloconAndroid/deeplinks-no-op/build.gradle.kts new file mode 100644 index 000000000..1d0039cd9 --- /dev/null +++ b/FloconAndroid/deeplinks-no-op/build.gradle.kts @@ -0,0 +1,118 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon-base")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "io.github.openflocon.flocon.deeplinks.noop" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-deeplinks-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Deeplinks No-Op" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt b/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt new file mode 100644 index 000000000..a377c7896 --- /dev/null +++ b/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt @@ -0,0 +1,35 @@ +package io.github.openflocon.flocon.plugins.deeplinks + +import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel + +actual object FloconDeeplinks : FloconPluginFactory { + override val name: String = "Deeplinks" + override val pluginId: String = null + override fun createConfig() = FloconDeeplinksConfig() + override fun install(config: FloconDeeplinksConfig, app: FloconApp): FloconDeeplinksPlugin { + return FloconDeeplinksPluginNoOp + } +} + +private object FloconDeeplinksPluginNoOp : FloconDeeplinksPlugin { + override fun registerDeeplinks(deeplinks: List) { + // no-op + } + + override fun onMessageReceived(method: String, body: String) { + // no-op + } + + override fun onConnectedToServer() { + // no-op + } +} + +fun floconRegisterDeeplink(vararg deeplinks: String) { + // no-op +} + +fun floconRegisterDeeplinks(deeplinks: List) { + // no-op +} diff --git a/FloconAndroid/deeplinks/build.gradle.kts b/FloconAndroid/deeplinks/build.gradle.kts new file mode 100644 index 000000000..fc1029164 --- /dev/null +++ b/FloconAndroid/deeplinks/build.gradle.kts @@ -0,0 +1,121 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon-base")) + implementation(project(":flocon")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(libs.kotlinx.serialization.json) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "io.github.openflocon.flocon.deeplinks" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-deeplinks", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Deeplinks" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt new file mode 100644 index 000000000..353080db9 --- /dev/null +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -0,0 +1,68 @@ +package io.github.openflocon.flocon.plugins.deeplinks + +import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +object FloconDeeplinks : FloconPluginFactory { + override val name: String = "Deeplinks" + override val pluginId: String = FloconDeeplinks::class.simpleName!! + override fun createConfig() = FloconDeeplinksConfig() + override fun install(config: FloconDeeplinksConfig, app: FloconApp): FloconDeeplinksPlugin { + val plugin = FloconDeeplinksPluginImpl( + sender = app.client as FloconMessageSender + ) + if (config.deeplinks.isNotEmpty()) { + plugin.registerDeeplinks(config.deeplinks) + } + return plugin + } +} + +internal class FloconDeeplinksPluginImpl( + private val sender: FloconMessageSender, +) : FloconPlugin, FloconDeeplinksPlugin { + + private val deeplinks = MutableStateFlow?>(null) + + override fun onMessageReceived( + method: String, + body: String, + ) { + // no op + } + + override fun onConnectedToServer() { + // on connected, send known deeplinks + deeplinks.value?.let { + registerDeeplinks(it) + } + } + + override fun registerDeeplinks(deeplinks: List) { + this.deeplinks.update { + deeplinks + } + + try { + sender.send( + plugin = Protocol.FromDevice.Deeplink.Plugin, + method = Protocol.FromDevice.Deeplink.Method.GetDeeplinks, + body = toDeeplinksJson(deeplinks) + ) + } catch (t: Throwable) { + FloconLogger.logError("deeplink mapping error", t) + } + } +} + +fun floconRegisterDeeplink(vararg deeplinks: String) { + val models = deeplinks.map { DeeplinkModel(link = it, parameters = emptyList()) } + FloconApp.instance?.client?.deeplinksPlugin?.registerDeeplinks(models) +} + +fun floconRegisterDeeplinks(deeplinks: List) { + FloconApp.instance?.client?.deeplinksPlugin?.registerDeeplinks(deeplinks) +} diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt new file mode 100644 index 000000000..dd57bb442 --- /dev/null +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt @@ -0,0 +1,25 @@ +package io.github.openflocon.flocon.plugins.deeplinks + +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkRemote +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinksRemote +import kotlinx.serialization.encodeToString + +internal fun toDeeplinksJson(deeplinks: List): String { + val dto = DeeplinksRemote(deeplinks.map { it.toRemote() }) + return FloconEncoder.json.encodeToString(dto) +} + +internal fun DeeplinkModel.toRemote(): DeeplinkRemote = DeeplinkRemote( + label = label, + link = link, + description = description, + parameters = parameters.map { it.toRemote() } +) + +internal fun DeeplinkModel.Parameter.toRemote(): DeeplinkParameterRemote = DeeplinkParameterRemote( + paramName = paramName, + autoComplete = autoComplete +) diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt new file mode 100644 index 000000000..bf521ba14 --- /dev/null +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt @@ -0,0 +1,13 @@ +package io.github.openflocon.flocon.plugins.deeplinks.model + +data class DeeplinkModel( + val link: String, + val label: String? = null, + val description: String? = null, + val parameters: List, +) { + data class Parameter( + val paramName: String, + val autoComplete: List, + ) +} diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt new file mode 100644 index 000000000..06cd4cf38 --- /dev/null +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.flocon.plugins.deeplinks.model + +import kotlinx.serialization.Serializable + +@Serializable +internal class DeeplinkParameterRemote( + val paramName: String, + val autoComplete: List, +) + +@Serializable +internal class DeeplinkRemote( + val label: String? = null, + val link: String, + val description: String? = null, + val parameters: List, +) + +@Serializable +internal class DeeplinksRemote( + val deeplinks: List, +) diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt index 2add1f3ba..b6df35c73 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt @@ -12,6 +12,7 @@ import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin import kotlinx.coroutines.flow.StateFlow abstract class FloconApp { + lateinit var context: FloconContext companion object { var instance: FloconApp? = null @@ -24,22 +25,28 @@ abstract class FloconApp { suspend fun connect(onClosed: () -> Unit) suspend fun disconnect() - val databasePlugin: FloconDatabasePlugin - val dashboardPlugin: FloconDashboardPlugin - val tablePlugin: FloconTablePlugin - val deeplinksPlugin: FloconDeeplinksPlugin - val analyticsPlugin: FloconAnalyticsPlugin - val networkPlugin: FloconNetworkPlugin - val devicePlugin: FloconDevicePlugin - val preferencesPlugin: FloconPreferencesPlugin - val crashReporterPlugin: FloconCrashReporterPlugin + val databasePlugin: FloconDatabasePlugin? + val dashboardPlugin: FloconDashboardPlugin? + val tablePlugin: FloconTablePlugin? + val deeplinksPlugin: FloconDeeplinksPlugin? + val analyticsPlugin: FloconAnalyticsPlugin? + val networkPlugin: FloconNetworkPlugin? + val devicePlugin: FloconDevicePlugin? + val preferencesPlugin: FloconPreferencesPlugin? + val crashReporterPlugin: FloconCrashReporterPlugin? + + /** + * Retrieve a plugin instance by its [key]. + */ + fun getPlugin(key: FloconPluginKey<*, T>): T? } open val client: Client? = null abstract val isInitialized : StateFlow - protected fun initializeFlocon() { + protected fun initializeFlocon(context: FloconContext) { + this.context = context instance = this } diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt new file mode 100644 index 000000000..b22025e11 --- /dev/null +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -0,0 +1,27 @@ +package io.github.openflocon.flocon + +/** + * Configuration builder for Flocon SDK. + * Used in [Flocon.initialize] to configure the SDK and install plugins. + */ +class FloconConfiguration internal constructor() { + internal val pluginConfigs = mutableMapOf, Any>() + + /** + * Install a plugin with the given [factory] and optional [configure] block. + */ + fun install( + factory: FloconPluginFactory, + configure: Config.() -> Unit = {} + ) { + val config = factory.createConfig() + config.configure() + pluginConfigs[factory] = config + } +} + +fun flocon(block: FloconConfiguration.() -> Unit) { + val configuration = FloconConfiguration().apply(block) + + +} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt new file mode 100644 index 000000000..4771c2713 --- /dev/null +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -0,0 +1,37 @@ +package io.github.openflocon.flocon + +/** + * Base interface for all Flocon plugins. + * Plugins can receive messages from the server and react to connection events. + */ +interface FloconPlugin { + fun onMessageReceived( + method: String, + body: String, + ) + fun onConnectedToServer() +} + +/** + * A unique key for identifying a Flocon plugin. + */ +interface FloconPluginKey { + val name: String + val pluginId: String? get() = null +} + +/** + * A factory for creating and installing Flocon plugins. + * This is the entry point for Ktor-style [install] calls. + */ +interface FloconPluginFactory : FloconPluginKey { + /** + * Create a default configuration instance for the plugin. + */ + fun createConfig(): Config + + /** + * Install the plugin into the [FloconApp] instance with the given [config]. + */ + fun install(config: Config, app: FloconApp): PluginInstance +} diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index ffc29c3ff..846d49bf4 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -1,9 +1,24 @@ package io.github.openflocon.flocon.plugins.analytics -import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.plugins.analytics.builder.AnalyticsBuilder import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem +class FloconAnalyticsConfig + +/** + * Flocon Analytics Plugin. + */ +expect object FloconAnalytics : FloconPluginFactory { + override fun createConfig(): FloconAnalyticsConfig + override fun install( + config: FloconAnalyticsConfig, + app: FloconApp + ): FloconAnalyticsPlugin + + override val name: String +} + fun floconAnalytics(analyticsName: String) : AnalyticsBuilder { return AnalyticsBuilder( analyticsTableId = analyticsName, @@ -18,6 +33,6 @@ fun FloconApp.analytics(analyticsName: String): AnalyticsBuilder { ) } -interface FloconAnalyticsPlugin { +interface FloconAnalyticsPlugin : FloconPlugin { fun registerAnalytics(analyticsItems: List) } \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt index 1c0f5c435..3d1fa8dd9 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt @@ -1,3 +1,16 @@ package io.github.openflocon.flocon.plugins.crashreporter -interface FloconCrashReporterPlugin \ No newline at end of file +import io.github.openflocon.flocon.* + +class FloconCrashReporterConfig { + var catchFatalErrors: Boolean = true +} + +/** + * Flocon Crash Reporter Plugin. + */ +expect object FloconCrashReporter : FloconPluginFactory + +interface FloconCrashReporterPlugin : FloconPlugin { + fun setupCrashHandler() +} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt index 20ac34fb7..bf2054d79 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt @@ -1,7 +1,15 @@ package io.github.openflocon.flocon.plugins.dashboard +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig -interface FloconDashboardPlugin { +class FloconDashboardConfig + +/** + * Flocon Dashboard Plugin. + */ +expect object FloconDashboard : FloconPluginFactory + +interface FloconDashboardPlugin : FloconPlugin { fun registerDashboard(dashboardConfig: DashboardConfig) } diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt index a300972b0..06b7acc6c 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt @@ -1,9 +1,17 @@ package io.github.openflocon.flocon.plugins.database -import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel import io.github.openflocon.flocon.plugins.database.model.FloconFileDatabaseModel +class FloconDatabaseConfig + +/** + * Flocon Database Plugin. + * Used to inspect Room or other SQL databases. + */ +expect object FloconDatabase : FloconPluginFactory + fun floconRegisterDatabase(database: FloconDatabaseModel) { FloconApp.instance?.client?.databasePlugin?.register( database @@ -27,7 +35,7 @@ fun floconLogDatabaseQuery(dbName: String, sqlQuery: String, bindArgs: List) } \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index e1bfbad78..316359861 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.plugins.deeplinks -import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel class DeeplinkLinkBuilder internal constructor( diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt index 37beaf2ae..c099eaa66 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt @@ -1,5 +1,14 @@ package io.github.openflocon.flocon.plugins.device -interface FloconDevicePlugin { +import io.github.openflocon.flocon.* + +class FloconDeviceConfig + +/** + * Flocon Device Plugin. + */ +expect object FloconDevice : FloconPluginFactory + +interface FloconDevicePlugin : FloconPlugin { fun registerWithSerial(serial: String) } \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt index 34f3cf447..3645820c5 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt @@ -1,3 +1,15 @@ package io.github.openflocon.flocon.plugins.files -interface FloconFilesPlugin \ No newline at end of file +import io.github.openflocon.flocon.* + +class FloconFilesConfig { + val roots = mutableListOf() +} + +/** + * Flocon Files Plugin. + * Used to inspect and download files from the device. + */ +expect object FloconFiles : FloconPluginFactory + +interface FloconFilesPlugin : FloconPlugin \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt index 76daf51fe..aaaf34432 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.plugins.network -import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.* 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 @@ -8,11 +8,22 @@ import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketEvent import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketMockListener import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse +class FloconNetworkConfig { + var badQualityConfig: BadQualityConfig? = null + val mocks = mutableListOf() +} + +/** + * Flocon Network Plugin. + * Used to inspect HTTP/S and WebSocket calls. + */ +expect object FloconNetwork : FloconPluginFactory + fun floconLogWebSocketEvent(event: FloconWebSocketEvent) { FloconApp.instance?.client?.networkPlugin?.logWebSocket(event) } -interface FloconNetworkPlugin { +interface FloconNetworkPlugin : FloconPlugin { val mocks: Collection val badQualityConfig: BadQualityConfig? diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt index e3893af2d..416f2cad5 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -1,12 +1,20 @@ package io.github.openflocon.flocon.plugins.sharedprefs -import io.github.openflocon.flocon.FloconApp -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference +import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconSharedPreferenceModel -fun floconRegisterPreference(preference: FloconPreference) { - FloconApp.instance?.client?.preferencesPlugin?.register(preference) +class FloconPreferencesConfig + +/** + * Flocon Preferences Plugin. + * Used to inspect SharedPreferences or other key-value stores. + */ +expect object FloconPreferences : FloconPluginFactory + +fun floconRegisterSharedPreference(sharedPreference: FloconSharedPreferenceModel) { + FloconApp.instance?.client?.preferencesPlugin?.register(sharedPreference) } -interface FloconPreferencesPlugin { - fun register(preference: FloconPreference) +interface FloconPreferencesPlugin : FloconPlugin { + fun register(sharedPreference: FloconSharedPreferenceModel) } \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt index 188adf89e..ad5d0ec9e 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt @@ -1,23 +1,31 @@ package io.github.openflocon.flocon.plugins.tables -import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.plugins.tables.builder.TableBuilder import io.github.openflocon.flocon.plugins.tables.model.TableItem -fun floconTable(tableName: String): TableBuilder { +class FloconTableConfig + +/** + * Flocon Table Plugin. + * Used to display custom data tables. + */ +expect object FloconTable : FloconPluginFactory + +fun floconTable(tableName: String) : TableBuilder { return TableBuilder( - tableName = tableName, + tableId = tableName, tablePlugin = FloconApp.instance?.client?.tablePlugin, ) } fun FloconApp.table(tableName: String): TableBuilder { return TableBuilder( - tableName = tableName, + tableId = tableName, tablePlugin = this.client?.tablePlugin, ) } -interface FloconTablePlugin { - fun registerTable(tableItem: TableItem) +interface FloconTablePlugin : FloconPlugin { + fun registerItems(tableItems: List) } \ No newline at end of file diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index 337f2b1e9..c887d14c2 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) implementation(libs.kotlinx.serialization.json) api(project(":flocon-base")) + api(project(":deeplinks")) } } diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.kt index 5490c40e4..85a2d610d 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -1,25 +1,13 @@ package io.github.openflocon.flocon import android.content.Context -import io.github.openflocon.flocon.client.FloconClientImpl -import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.plugins.dashboard.FloconDashboardPlugin -import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinksPlugin -import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext object Flocon : FloconCore() { - fun initialize(context: Context) { + fun initialize(context: Context, block: FloconConfiguration.() -> Unit = {}) { + val configuration = FloconConfiguration().apply(block) super.initializeFlocon( - FloconContext(appContext = context) + context = FloconContext(appContext = context), + configuration = configuration ) } -} \ No newline at end of file +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt index ee40db561..f9bf17c23 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt @@ -28,12 +28,16 @@ abstract class FloconCore : FloconApp() { private val _isInitialized = MutableStateFlow(false) override val isInitialized: StateFlow = _isInitialized - protected fun initializeFlocon(context: FloconContext) { - val newClient = FloconClientImpl(context) + protected fun initializeFlocon( + context: FloconContext, + configuration: FloconConfiguration = FloconConfiguration() + ) { + super.initializeFlocon(context) + val newClient = FloconClientImpl(context, configuration) _client = newClient // Setup crash handler early to catch crashes during initialization - newClient.crashReporterPlugin.setupCrashHandler() + newClient.crashReporterPlugin?.setupCrashHandler() _isInitialized.value = true diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt index ad0ddcdc9..9778ea7e3 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt @@ -1,28 +1,28 @@ package io.github.openflocon.flocon.client -import io.github.openflocon.flocon.FloconApp -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconFile -import io.github.openflocon.flocon.Protocol -import io.github.openflocon.flocon.core.FloconFileSender -import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -import io.github.openflocon.flocon.core.getAppInfos -import io.github.openflocon.flocon.getServerHost -import io.github.openflocon.flocon.model.FloconFileInfo -import io.github.openflocon.flocon.model.FloconMessageToServer -import io.github.openflocon.flocon.model.floconMessageFromServerFromJson -import io.github.openflocon.flocon.model.toFloconMessageToServer -import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPluginImpl -import io.github.openflocon.flocon.plugins.dashboard.FloconDashboardPluginImpl -import io.github.openflocon.flocon.plugins.database.FloconDatabasePluginImpl -import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinksPluginImpl -import io.github.openflocon.flocon.plugins.device.FloconDevicePluginImpl -import io.github.openflocon.flocon.plugins.files.FloconFilesPluginImpl -import io.github.openflocon.flocon.plugins.network.FloconNetworkPluginImpl -import io.github.openflocon.flocon.plugins.sharedprefs.FloconPreferencesPluginImpl -import io.github.openflocon.flocon.plugins.tables.FloconTablePluginImpl -import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporterPluginImpl +import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.core.* +import io.github.openflocon.flocon.model.* +import io.github.openflocon.flocon.plugins.analytics.FloconAnalytics +import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin +import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporter +import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporterPlugin +import io.github.openflocon.flocon.plugins.dashboard.FloconDashboard +import io.github.openflocon.flocon.plugins.dashboard.FloconDashboardPlugin +import io.github.openflocon.flocon.plugins.database.FloconDatabase +import io.github.openflocon.flocon.plugins.database.FloconDatabasePlugin +import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks +import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinksPlugin +import io.github.openflocon.flocon.plugins.device.FloconDevice +import io.github.openflocon.flocon.plugins.device.FloconDevicePlugin +import io.github.openflocon.flocon.plugins.files.FloconFiles +import io.github.openflocon.flocon.plugins.files.FloconFilesPlugin +import io.github.openflocon.flocon.plugins.network.FloconNetwork +import io.github.openflocon.flocon.plugins.network.FloconNetworkPlugin +import io.github.openflocon.flocon.plugins.sharedprefs.FloconPreferences +import io.github.openflocon.flocon.plugins.sharedprefs.FloconPreferencesPlugin +import io.github.openflocon.flocon.plugins.tables.FloconTable +import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin import io.github.openflocon.flocon.utils.currentTimeMillis import io.github.openflocon.flocon.websocket.FloconHttpClient import io.github.openflocon.flocon.websocket.FloconWebSocketClient @@ -34,17 +34,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import kotlin.getValue internal class FloconClientImpl( private val appContext: FloconContext, + private val configuration: FloconConfiguration, ) : FloconApp.Client, FloconMessageSender, FloconFileSender { private val FLOCON_WEBSOCKET_PORT = 9023 private val FLOCON_HTTP_PORT = 9024 private val appInstance by lazy { - // store the start time of the sdk, for this app launch currentTimeMillis() } @@ -64,38 +63,37 @@ internal class FloconClientImpl( } private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - // region plugins - override val databasePlugin = FloconDatabasePluginImpl(context = appContext, sender = this) - private val filesPlugin = FloconFilesPluginImpl(context = appContext, sender = this, floconFileSender = this) - override val preferencesPlugin = FloconPreferencesPluginImpl(context = appContext, sender = this, scope = coroutineScope) - override val dashboardPlugin = FloconDashboardPluginImpl(sender = this) - override val tablePlugin = FloconTablePluginImpl(sender = this) - override val deeplinksPlugin = FloconDeeplinksPluginImpl(sender = this) - override val analyticsPlugin = FloconAnalyticsPluginImpl(sender = this) - override val devicePlugin = FloconDevicePluginImpl(sender = this, context = appContext) - override val networkPlugin = FloconNetworkPluginImpl( - context = appContext, - sender = this, - coroutineScope = coroutineScope, - ) - override val crashReporterPlugin = FloconCrashReporterPluginImpl( - context = appContext, - sender = this, - coroutineScope = coroutineScope, - ) - - private val allPlugins = listOf( - databasePlugin, - filesPlugin, - preferencesPlugin, - dashboardPlugin, - tablePlugin, - deeplinksPlugin, - analyticsPlugin, - networkPlugin, - devicePlugin, - crashReporterPlugin, - ) + private val installedPlugins = mutableMapOf, Any>() + private val pluginIdToPlugin = mutableMapOf() + + init { + configuration.pluginConfigs.forEach { (factory, config) -> + @Suppress("UNCHECKED_CAST") + val plugin = (factory as FloconPluginFactory).install(config, FloconApp.instance!!) + installedPlugins[factory] = plugin + factory.pluginId?.let { id -> + if (plugin is FloconPlugin) { + pluginIdToPlugin[id] = plugin + } + } + } + } + + override fun getPlugin(key: FloconPluginKey<*, T>): T? { + return installedPlugins[key] as? T + } + + // region plugins backward compatibility + override val databasePlugin: FloconDatabasePlugin? get() = getPlugin(FloconDatabase) + override val dashboardPlugin: FloconDashboardPlugin? get() = getPlugin(FloconDashboard) + override val tablePlugin: FloconTablePlugin? get() = getPlugin(FloconTable) + override val deeplinksPlugin: FloconDeeplinksPlugin? get() = getPlugin(FloconDeeplinks) + override val analyticsPlugin: FloconAnalyticsPlugin? get() = getPlugin(FloconAnalytics) + override val networkPlugin: FloconNetworkPlugin? get() = getPlugin(FloconNetwork) + override val devicePlugin: FloconDevicePlugin? get() = getPlugin(FloconDevice) + override val preferencesPlugin: FloconPreferencesPlugin? get() = getPlugin(FloconPreferences) + override val crashReporterPlugin: FloconCrashReporterPlugin? get() = getPlugin(FloconCrashReporter) + // endregion @Throws(Throwable::class) override suspend fun connect( @@ -107,8 +105,10 @@ internal class FloconClientImpl( onMessageReceived = ::onMessageReceived, onClosed = onClosed, ) - allPlugins.forEach { - it.onConnectedToServer() + installedPlugins.values.forEach { + if (it is FloconPlugin) { + it.onConnectedToServer() + } } } @@ -119,55 +119,10 @@ internal class FloconClientImpl( private fun onMessageReceived(message: String) { coroutineScope.launch(Dispatchers.IO) { floconMessageFromServerFromJson(message)?.let { messageFromServer -> - when (messageFromServer.plugin) { - Protocol.ToDevice.Database.Plugin -> { - databasePlugin.onMessageReceived( - messageFromServer = messageFromServer, - ) - } - - Protocol.ToDevice.Files.Plugin -> { - filesPlugin.onMessageReceived( - messageFromServer = messageFromServer, - ) - } - - Protocol.ToDevice.SharedPreferences.Plugin -> { - preferencesPlugin.onMessageReceived( - messageFromServer = messageFromServer, - ) - } - - Protocol.ToDevice.Device.Plugin -> { - devicePlugin.onMessageReceived( - messageFromServer = messageFromServer, - ) - } - - Protocol.ToDevice.Dashboard.Plugin -> { - dashboardPlugin.onMessageReceived( - messageFromServer = messageFromServer, - ) - } - - Protocol.ToDevice.Table.Plugin -> { - tablePlugin.onMessageReceived( - messageFromServer = messageFromServer, - ) - } - - Protocol.ToDevice.Analytics.Plugin -> { - analyticsPlugin.onMessageReceived( - messageFromServer = messageFromServer, - ) - } - - Protocol.ToDevice.Network.Plugin -> { - networkPlugin.onMessageReceived( - messageFromServer = messageFromServer, - ) - } - } + pluginIdToPlugin[messageFromServer.plugin]?.onMessageReceived( + method = messageFromServer.method, + body = messageFromServer.body, + ) } } } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt index 68aab88d3..679355f17 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass -internal object FloconEncoder { +object FloconEncoder { val json = Json { ignoreUnknownKeys = true isLenient = true diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt index 2038fdade..4e99fbc91 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.core -internal interface FloconMessageSender { +interface FloconMessageSender { fun send( plugin: String, method: String, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index 538bdb529..4ffa613ed 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -1,19 +1,28 @@ package io.github.openflocon.flocon.plugins.analytics -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem import io.github.openflocon.flocon.plugins.analytics.mapper.analyticsItemsToJson +actual object FloconAnalytics : FloconPluginFactory { + override val name: String = "Analytics" + override val pluginId: String = Protocol.ToDevice.Analytics.Plugin + override fun createConfig() = FloconAnalyticsConfig() + override fun install(config: FloconAnalyticsConfig, app: FloconApp): FloconAnalyticsPlugin { + return FloconAnalyticsPluginImpl( + sender = app.client as FloconMessageSender + ) + } +} + internal class FloconAnalyticsPluginImpl( private val sender: FloconMessageSender, ) : FloconPlugin, FloconAnalyticsPlugin { override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { // no op } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt index 072d35521..583761f42 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt @@ -1,20 +1,32 @@ package io.github.openflocon.flocon.plugins.crashreporter -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel import io.github.openflocon.flocon.plugins.crashreporter.model.crashReportsListToJson import io.github.openflocon.flocon.utils.currentTimeMillis -import io.github.openflocon.flocondesktop.BuildConfig import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +actual object FloconCrashReporter : FloconPluginFactory { + override val name: String = "CrashReporter" + override val pluginId: String = Protocol.ToDevice.Analytics.Plugin // Crash reporter is usually write-only but we can set an ID + override fun createConfig() = FloconCrashReporterConfig() + override fun install(config: FloconCrashReporterConfig, app: FloconApp): FloconCrashReporterPlugin { + val client = app.client as FloconMessageSender + return FloconCrashReporterPluginImpl( + context = FloconContext(appContext = null), // Handled by datasource + sender = client, + coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + ) + } +} + internal class FloconCrashReporterPluginImpl( private val context: FloconContext, private var sender: FloconMessageSender, @@ -23,7 +35,7 @@ internal class FloconCrashReporterPluginImpl( private val dataSource = buildFloconCrashReporterDataSource(context) - fun setupCrashHandler() { + override fun setupCrashHandler() { setupUncaughtExceptionHandler(context) { throwable -> val crash = createCrashReport(throwable) dataSource.saveCrash(crash) @@ -46,7 +58,10 @@ internal class FloconCrashReporterPluginImpl( } } - override fun onMessageReceived(messageFromServer: FloconMessageFromServer) { + override fun onMessageReceived( + method: String, + body: String, + ) { // No messages from desktop for crashes (write-only plugin) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt index d1e15e600..c4fe95c04 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt @@ -1,10 +1,7 @@ package io.github.openflocon.flocon.plugins.dashboard -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.dashboard.mapper.toJson import io.github.openflocon.flocon.plugins.dashboard.model.DashboardCallback import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig @@ -16,6 +13,17 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +actual object FloconDashboard : FloconPluginFactory { + override val name: String = "Dashboard" + override val pluginId: String = Protocol.ToDevice.Dashboard.Plugin + override fun createConfig() = FloconDashboardConfig() + override fun install(config: FloconDashboardConfig, app: FloconApp): FloconDashboardPlugin { + return FloconDashboardPluginImpl( + sender = app.client as FloconMessageSender + ) + } +} + internal class FloconDashboardPluginImpl( private val sender: FloconMessageSender, ) : FloconPlugin, FloconDashboardPlugin { @@ -28,17 +36,18 @@ internal class FloconDashboardPluginImpl( private val callbackMap = mutableMapOf() override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { scope.launch { - when (messageFromServer.method) { + when (method) { Protocol.ToDevice.Dashboard.Method.OnClick -> { - val id = messageFromServer.body + val id = body callbackMap[id]?.let { it as? DashboardCallback.ButtonCallback }?.action?.invoke() } Protocol.ToDevice.Dashboard.Method.OnFormSubmitted -> { - ToDeviceSubmittedFormMessage.fromJson(messageFromServer.body)?.let { + ToDeviceSubmittedFormMessage.fromJson(body)?.let { callbackMap[it.id]?.let { it as? DashboardCallback.FormCallback }?.actions?.invoke( it.values ) @@ -46,7 +55,7 @@ internal class FloconDashboardPluginImpl( } Protocol.ToDevice.Dashboard.Method.OnTextFieldSubmitted -> { - ToDeviceSubmittedTextFieldMessage.fromJson(messageFromServer.body)?.let { + ToDeviceSubmittedTextFieldMessage.fromJson(body)?.let { callbackMap[it.id]?.let { it as? DashboardCallback.TextFieldCallback }?.action?.invoke( it.value ) @@ -54,7 +63,7 @@ internal class FloconDashboardPluginImpl( } Protocol.ToDevice.Dashboard.Method.OnCheckBoxValueChanged -> { - ToDeviceCheckBoxValueChangedMessage.fromJson(messageFromServer.body)?.let { + ToDeviceCheckBoxValueChangedMessage.fromJson(body)?.let { callbackMap[it.id]?.let { it as? DashboardCallback.CheckBoxCallback }?.action?.invoke( it.value ) @@ -97,4 +106,3 @@ internal class FloconDashboardPluginImpl( } } } - diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt index 10c97c58c..c976630a8 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt @@ -1,10 +1,7 @@ package io.github.openflocon.flocon.plugins.database -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse @@ -32,6 +29,18 @@ internal interface FloconDatabaseDataSource { internal expect fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource +actual object FloconDatabase : FloconPluginFactory { + override val name: String = "Database" + override val pluginId: String = Protocol.ToDevice.Database.Plugin + override fun createConfig() = FloconDatabaseConfig() + override fun install(config: FloconDatabaseConfig, app: FloconApp): FloconDatabasePlugin { + return FloconDatabasePluginImpl( + sender = app.client as FloconMessageSender, + context = FloconContext(appContext = null), // Handled by actual buildFloconDatabaseDataSource + ) + } +} + internal class FloconDatabasePluginImpl( private var sender: FloconMessageSender, private val context: FloconContext, @@ -42,16 +51,17 @@ internal class FloconDatabasePluginImpl( private val dataSource = buildFloconDatabaseDataSource(context) override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { - when (messageFromServer.method) { + when (method) { Protocol.ToDevice.Database.Method.GetDatabases -> { sendAllDatabases(sender) } Protocol.ToDevice.Database.Method.Query -> { val queryMessage = - DatabaseQueryMessage.fromJson(message = messageFromServer.body) ?: return + DatabaseQueryMessage.fromJson(message = body) ?: return val result = dataSource.executeSQL( registeredDatabases = registeredDatabases.value, databaseName = queryMessage.database, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index b65bc40fc..9df144790 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -1,14 +1,27 @@ package io.github.openflocon.flocon.plugins.deeplinks -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel +import io.github.openflocon.flocon.plugins.deeplinks.mapper.toDeeplinksJson import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +actual object FloconDeeplinks : FloconPluginFactory { + override val name: String = "Deeplinks" + override val pluginId: String = Protocol.ToDevice.Deeplink.Plugin + override fun createConfig() = FloconDeeplinksConfig() + override fun install(config: FloconDeeplinksConfig, app: FloconApp): FloconDeeplinksPlugin { + val plugin = FloconDeeplinksPluginImpl( + sender = app.client as FloconMessageSender + ) + if (config.deeplinks.isNotEmpty()) { + plugin.registerDeeplinks(config.deeplinks) + } + return plugin + } +} + internal class FloconDeeplinksPluginImpl( private val sender: FloconMessageSender, ) : FloconPlugin, FloconDeeplinksPlugin { @@ -17,13 +30,14 @@ internal class FloconDeeplinksPluginImpl( private val variables = MutableStateFlow?>(null) override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { - + // no op } override fun onConnectedToServer() { - // on connected, send known dashboard + // on connected, send known deeplinks deeplinks.value?.let { registerDeeplinks(it, variables.value.orEmpty()) } @@ -50,4 +64,3 @@ internal class FloconDeeplinksPluginImpl( } } } - diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt index 4a2e02b15..f67e4581b 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt @@ -1,13 +1,21 @@ package io.github.openflocon.flocon.plugins.device -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.device.model.fromdevice.RegisterDeviceDataModel +actual object FloconDevice : FloconPluginFactory { + override val name: String = "Device" + override val pluginId: String = Protocol.ToDevice.Device.Plugin + override fun createConfig() = FloconDeviceConfig() + override fun install(config: FloconDeviceConfig, app: FloconApp): FloconDevicePlugin { + return FloconDevicePluginImpl( + sender = app.client as FloconMessageSender, + context = app.context + ) + } +} + internal expect fun restartApp(context: FloconContext) internal class FloconDevicePluginImpl( @@ -28,9 +36,10 @@ internal class FloconDevicePluginImpl( } override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { - when (messageFromServer.method) { + when (method) { Protocol.ToDevice.Device.Method.GetAppIcon -> { val icon = getAppIconBase64(context) if (icon != null) { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt index 6be68c2d8..7761af4b0 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt @@ -1,14 +1,9 @@ package io.github.openflocon.flocon.plugins.files -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconFile -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconFileSender import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin import io.github.openflocon.flocon.model.FloconFileInfo -import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.files.model.fromdevice.FileDataModel import io.github.openflocon.flocon.plugins.files.model.fromdevice.FilesResultDataModel import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceDeleteFileMessage @@ -19,6 +14,20 @@ import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceGetFiles import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +actual object FloconFiles : FloconPluginFactory { + override val name: String = "Files" + override val pluginId: String = Protocol.ToDevice.Files.Plugin + override fun createConfig() = FloconFilesConfig() + override fun install(config: FloconFilesConfig, app: FloconApp): FloconFilesPlugin { + val client = app.client + return FloconFilesPluginImpl( + context = app.context, + floconFileSender = client as FloconFileSender, + sender = client as FloconMessageSender + ) + } +} + internal interface FileDataSource { fun getFile(path: String, isConstantPath: Boolean): FloconFile? fun getFolderContent(path: String, isConstantPath: Boolean, withFoldersSize: Boolean): List @@ -39,11 +48,12 @@ internal class FloconFilesPluginImpl( private val withFoldersSize = MutableStateFlow(false) override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { - when (messageFromServer.method) { + when (method) { Protocol.ToDevice.Files.Method.ListFiles -> { - val listFilesMessage = ToDeviceGetFilesMessage.fromJson(message = messageFromServer.body) ?: return + val listFilesMessage = ToDeviceGetFilesMessage.fromJson(message = body) ?: return withFoldersSize.update { listFilesMessage.withFoldersSize } @@ -55,7 +65,7 @@ internal class FloconFilesPluginImpl( } Protocol.ToDevice.Files.Method.GetFile -> { - val getFileMessage = ToDeviceGetFileMessage.fromJson(message = messageFromServer.body) ?: return + val getFileMessage = ToDeviceGetFileMessage.fromJson(message = body) ?: return fileDataSource.getFile(path = getFileMessage.path, isConstantPath = false)?.let { file -> floconFileSender.send( @@ -70,7 +80,7 @@ internal class FloconFilesPluginImpl( Protocol.ToDevice.Files.Method.DeleteFile -> { val deleteFilesMessage = - ToDeviceDeleteFileMessage.fromJson(message = messageFromServer.body) ?: return + ToDeviceDeleteFileMessage.fromJson(message = body) ?: return fileDataSource.deleteFile( path = deleteFilesMessage.filePath, @@ -85,7 +95,7 @@ internal class FloconFilesPluginImpl( Protocol.ToDevice.Files.Method.DeleteFiles -> { val deleteFilesMessage = - ToDeviceDeleteFilesMessage.fromJson(message = messageFromServer.body) ?: return + ToDeviceDeleteFilesMessage.fromJson(message = body) ?: return fileDataSource.deleteFiles( path = deleteFilesMessage.filePaths, @@ -100,7 +110,7 @@ internal class FloconFilesPluginImpl( Protocol.ToDevice.Files.Method.DeleteFolderContent -> { val deleteFolderContentMessage = - ToDeviceDeleteFolderContentMessage.fromJson(message = messageFromServer.body) + ToDeviceDeleteFolderContentMessage.fromJson(message = body) ?: return fileDataSource.getFile( diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt index 4a58473f5..51f23c8cc 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt @@ -1,30 +1,31 @@ package io.github.openflocon.flocon.plugins.network -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -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.floconNetworkWebSocketEventToJson -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.parseWebSocketMockMessage -import io.github.openflocon.flocon.plugins.network.mapper.webSocketIdsToJsonArray -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.FloconWebSocketEvent -import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketMockListener -import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse +import io.github.openflocon.flocon.plugins.network.mapper.* +import io.github.openflocon.flocon.plugins.network.model.* import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +actual object FloconNetwork : FloconPluginFactory { + override val name: String = "Network" + override val pluginId: String = Protocol.ToDevice.Network.Plugin + override fun createConfig() = FloconNetworkConfig() + override fun install(config: FloconNetworkConfig, app: FloconApp): FloconNetworkPlugin { + return FloconNetworkPluginImpl( + context = app.context, + sender = app.client as FloconMessageSender, + coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + ) + } +} + internal const val FLOCON_NETWORK_MOCKS_JSON = "flocon_network_mocks.json" internal const val FLOCON_NETWORK_BAD_CONFIG_JSON = "flocon_network_bad_config.json" @@ -54,9 +55,7 @@ internal class FloconNetworkPluginImpl( private val _badQualityConfig = MutableStateFlow(dataSource.loadBadNetworkConfig()) override val badQualityConfig: BadQualityConfig? - get() { - return _badQualityConfig.value - } + get() = _badQualityConfig.value override fun logRequest(request: FloconNetworkCallRequest) { try { @@ -72,7 +71,7 @@ internal class FloconNetworkPluginImpl( override fun logResponse(response: FloconNetworkCallResponse) { coroutineScope.launch { - delay(200) // to be sure the request is handled before the response, in case of mocks or direct connection refused + delay(200) // to be sure the request is handled before the response try { sender.send( plugin = Protocol.FromDevice.Network.Plugin, @@ -102,23 +101,24 @@ internal class FloconNetworkPluginImpl( } override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { - when (messageFromServer.method) { + when (method) { Protocol.ToDevice.Network.Method.SetupMocks -> { - val setup = parseMockResponses(jsonString = messageFromServer.body) + val setup = parseMockResponses(jsonString = body) _mocks.update { setup } dataSource.saveMocksToFile(mocks) } Protocol.ToDevice.Network.Method.SetupBadNetworkConfig -> { - val config = parseBadQualityConfig(jsonString = messageFromServer.body) + val config = parseBadQualityConfig(jsonString = body) _badQualityConfig.update { config } dataSource.saveBadNetworkConfig(config) } Protocol.ToDevice.Network.Method.WebsocketMockMessage -> { - val message = parseWebSocketMockMessage(jsonString = messageFromServer.body) + val message = parseWebSocketMockMessage(jsonString = body) if(message != null) { websocketListeners.value[message.id]?.onMessage(message.message) } @@ -147,5 +147,4 @@ internal class FloconNetworkPluginImpl( body = webSocketIdsToJsonArray(ids = websocketListeners.value.keys), ) } - } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt index e1818d432..7b5f526f4 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -1,167 +1,85 @@ package io.github.openflocon.flocon.plugins.sharedprefs -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -import io.github.openflocon.flocon.model.FloconMessageFromServer -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreferenceValue -import io.github.openflocon.flocon.plugins.sharedprefs.model.fromdevice.PreferenceRowDataModel -import io.github.openflocon.flocon.plugins.sharedprefs.model.fromdevice.SharedPreferenceValueResultDataModel -import io.github.openflocon.flocon.plugins.sharedprefs.model.listSharedPreferencesDescriptorToJson -import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.ToDeviceEditSharedPreferenceValueMessage -import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.ToDeviceGetSharedPreferenceValueMessage -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import io.github.openflocon.flocon.plugins.sharedprefs.mapper.toJson +import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconSharedPreferenceModel +import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.SetSharedPreferenceValueMessage + +actual object FloconPreferences : FloconPluginFactory { + override val name: String = "Preferences" + override val pluginId: String = Protocol.ToDevice.SharedPreferences.Plugin + override fun createConfig() = FloconPreferencesConfig() + override fun install(config: FloconPreferencesConfig, app: FloconApp): FloconPreferencesPlugin { + return FloconSharedPrefsPluginImpl( + context = app.context, + sender = app.client as FloconMessageSender + ) + } +} -internal interface FloconPreferencesDataSource { - fun detectLocalPreferences(): List +internal interface FloconSharedPreferenceDataSource { + fun getSharedPreferences(): List + fun getSharedPreferenceValue(fileName: String, key: String): String? + fun setSharedPreferenceValue(fileName: String, key: String, value: String) } -internal expect fun buildFloconPreferencesDataSource(context: FloconContext): FloconPreferencesDataSource +internal expect fun buildFloconSharedPreferenceDataSource(context: FloconContext): FloconSharedPreferenceDataSource -internal class FloconPreferencesPluginImpl( - context: FloconContext, - private var sender: FloconMessageSender, - private val scope: CoroutineScope, +internal class FloconSharedPrefsPluginImpl( + private val context: FloconContext, + private val sender: FloconMessageSender, ) : FloconPlugin, FloconPreferencesPlugin { - // references for quick access - private val preferences = mutableMapOf() - - private val dataSource = buildFloconPreferencesDataSource(context) + private val dataSource = buildFloconSharedPreferenceDataSource(context) + private val preferenceModels = mutableListOf() override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { - when (messageFromServer.method) { + when (method) { Protocol.ToDevice.SharedPreferences.Method.GetSharedPreferences -> { - sendAllSharedPrefs() + sendSharedPreferences() } Protocol.ToDevice.SharedPreferences.Method.GetSharedPreferenceValue -> { - val toDeviceMessage = - ToDeviceGetSharedPreferenceValueMessage.fromJson(message = messageFromServer.body) - ?: return - - val preference = preferences[toDeviceMessage.sharedPreferenceName] ?: return - - scope.launch { - sendToServerPreferenceValues( - preference = preference, - requestId = toDeviceMessage.requestId, - sender = sender, - ) - } + // Not implemented yet on device side, usually handled by getSharedPreferences } Protocol.ToDevice.SharedPreferences.Method.SetSharedPreferenceValue -> { - val toDeviceMessage = - ToDeviceEditSharedPreferenceValueMessage.fromJson(jsonString = messageFromServer.body) - ?: return - - val preference = preferences[toDeviceMessage.sharedPreferenceName] ?: return - - scope.launch { - try { - preference.set( - columnName = toDeviceMessage.key, - value = FloconPreferenceValue( - stringValue = toDeviceMessage.stringValue, - booleanValue = toDeviceMessage.booleanValue, - intValue = toDeviceMessage.intValue, - longValue = toDeviceMessage.longValue, - floatValue = toDeviceMessage.floatValue, - setStringValue = toDeviceMessage.setStringValue, - ), - ) - - // then send the shared pref content - sendToServerPreferenceValues( - preference = preference, - requestId = toDeviceMessage.requestId, - sender = sender, - ) - - //sender.send(Protocol.FromDevice.SharedPreferences.Plugin, "success") - } catch (t: Throwable) { - t.printStackTrace() - //sender.send(Protocol.FromDevice.SharedPreferences.Plugin, "failure") - } + SetSharedPreferenceValueMessage.fromJson(body)?.let { message -> + dataSource.setSharedPreferenceValue( + fileName = message.fileName, + key = message.key, + value = message.value + ) + // Refresh view + sendSharedPreferences() } } } } - private suspend fun sendToServerPreferenceValues( - preference: FloconPreference, - requestId: String, - sender: FloconMessageSender - ) { - val columns = preference.columns() - val rows = columns.map { key -> - val value = preference.get(key) - PreferenceRowDataModel( - key = key, - stringValue = value?.stringValue, - intValue = value?.intValue, - floatValue = value?.floatValue, - booleanValue = value?.booleanValue, - longValue = value?.longValue, - setStringValue = value?.setStringValue, - ) - } - - try { - sender.send( - plugin = Protocol.FromDevice.SharedPreferences.Plugin, - method = Protocol.FromDevice.SharedPreferences.Method.GetSharedPreferenceValue, - body = SharedPreferenceValueResultDataModel( - requestId = requestId, - sharedPreferenceName = preference.name, - rows = rows, - ).toJson(), - ) - } catch (t: Throwable) { - FloconLogger.logError("SharedPreferences json mapping error", t) - } - } - - // on connected, send all shared prefs override fun onConnectedToServer() { - dataSource.detectLocalPreferences().forEach { preference -> - registerInternal(preference) - } - sendAllSharedPrefs() + sendSharedPreferences() } - override fun register(preference: FloconPreference) { - registerInternal(preference) - sendAllSharedPrefs() + override fun register(sharedPreference: FloconSharedPreferenceModel) { + preferenceModels.add(sharedPreference) + sendSharedPreferences() } - private fun registerInternal(preference: FloconPreference) { - if(preferences.containsKey(preference.name).not()) { - preferences[preference.name] = preference - } - } - - private fun sendAllSharedPrefs() { - val allPrefs = getAllPreferences() + private fun sendSharedPreferences() { + val allPrefs = dataSource.getSharedPreferences() + preferenceModels try { sender.send( plugin = Protocol.FromDevice.SharedPreferences.Plugin, method = Protocol.FromDevice.SharedPreferences.Method.GetSharedPreferences, - body = listSharedPreferencesDescriptorToJson(allPrefs).toString(), + body = allPrefs.toJson().toString() ) } catch (t: Throwable) { FloconLogger.logError("SharedPreferences json mapping error", t) } } - - private fun getAllPreferences(): List { - return preferences.values.sortedBy { it.name } - } } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt index e2d6a7ec4..c52587f62 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt @@ -1,19 +1,28 @@ package io.github.openflocon.flocon.plugins.tables -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.FloconPlugin -import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.tables.model.TableItem import io.github.openflocon.flocon.plugins.tables.model.tableItemListToJson +actual object FloconTable : FloconPluginFactory { + override val name: String = "Table" + override val pluginId: String = Protocol.ToDevice.Table.Plugin + override fun createConfig() = FloconTableConfig() + override fun install(config: FloconTableConfig, app: FloconApp): FloconTablePlugin { + return FloconTablePluginImpl( + sender = app.client as FloconMessageSender + ) + } +} + internal class FloconTablePluginImpl( private val sender: FloconMessageSender, ) : FloconPlugin, FloconTablePlugin { override fun onMessageReceived( - messageFromServer: FloconMessageFromServer, + method: String, + body: String, ) { // no op } @@ -22,16 +31,16 @@ internal class FloconTablePluginImpl( // no op } - override fun registerTable(tableItem: TableItem) { - sendTable(tableItem) + override fun registerItems(tableItems: List) { + sendTable(tableItems) } - private fun sendTable(tableItem: TableItem) { + private fun sendTable(tableItems: List) { try { sender.send( plugin = Protocol.FromDevice.Table.Plugin, method = Protocol.FromDevice.Table.Method.AddItems, - body = tableItemListToJson(listOf(tableItem)).toString() // desktop is expecting an array of table items + body = tableItemListToJson(tableItems).toString() ) } catch (t: Throwable) { FloconLogger.logError("Table json mapping error", t) diff --git a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt index 5878a2038..5cf459641 100644 --- a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt +++ b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt @@ -15,6 +15,8 @@ import io.github.openflocon.flocon.myapplication.multi.database.initializeDataba import io.github.openflocon.flocon.myapplication.multi.database.model.DogEntity import io.github.openflocon.flocon.myapplication.multi.sharedpreferences.initializeSharedPreferences import io.github.openflocon.flocon.myapplication.multi.ui.App +import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks +import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinksPlugin import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import kotlinx.coroutines.GlobalScope @@ -55,7 +57,11 @@ class MainActivity : ComponentActivity() { ) FloconLogger.enabled = true - Flocon.initialize(this) + Flocon.initialize(this) { + install(FloconDeeplinks) { + + } + } setContent { App() diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index 511b0712c..a0cd76fd5 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -29,3 +29,5 @@ include(":ktor-interceptor") include(":ktor-interceptor-no-op") include(":datastores") include(":datastores-no-op") +include(":deeplinks") +include(":deeplinks-no-op") From 12ef1441fb53ff436dacc80ffe36c3553c6923f5 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Wed, 11 Mar 2026 23:29:51 +0100 Subject: [PATCH 02/38] feat: Make app compile & remove flocon-base --- .../datastores-no-op/build.gradle.kts | 2 +- .../model/FloconDatastorePreference.kt | 19 +- FloconAndroid/datastores/build.gradle.kts | 2 +- .../model/FloconDatastorePreference.kt | 140 +++---- .../deeplinks-no-op/build.gradle.kts | 2 +- .../plugins/deeplinks/FloconDeeplinksNoOp.kt | 1 - FloconAndroid/deeplinks/build.gradle.kts | 1 - .../plugins/deeplinks/FloconDeeplinks.kt | 10 +- .../deeplinks/FloconDeeplinksPlugin.kt | 2 +- FloconAndroid/flocon-base/.gitignore | 1 - FloconAndroid/flocon-base/build.gradle.kts | 118 ------ FloconAndroid/flocon-base/consumer-rules.pro | 0 FloconAndroid/flocon-base/proguard-rules.pro | 21 -- .../io/github/openflocon/flocon/FloconApp.kt | 53 --- .../analytics/FloconAnalyticsPlugin.kt | 38 -- .../FloconCrashReporterPlugin.kt | 16 - .../dashboard/FloconDashboardPlugin.kt | 15 - .../dashboard/builder/ContainerBuilder.kt | 14 - .../flocon/plugins/dashboard/dsl/ButtonDsl.kt | 19 - .../flocon/plugins/dashboard/dsl/HtmlDsl.kt | 9 - .../plugins/dashboard/dsl/MarkdownDsl.kt | 9 - .../plugins/dashboard/dsl/PlainTextDsl.kt | 24 -- .../plugins/dashboard/dsl/SectionDsl.kt | 13 - .../flocon/plugins/dashboard/dsl/TextDsl.kt | 15 - .../plugins/dashboard/model/ContainerType.kt | 6 - .../plugins/dashboard/model/Dashboard.kt | 8 - .../dashboard/model/config/ContainerConfig.kt | 9 - .../dashboard/model/config/ElementConfig.kt | 3 - .../plugins/database/FloconDatabasePlugin.kt | 41 --- .../plugins/deeplinks/model/DeeplinkModel.kt | 24 -- .../plugins/device/FloconDevicePlugin.kt | 14 - .../flocon/plugins/files/FloconFilesPlugin.kt | 15 - .../plugins/network/FloconNetworkPlugin.kt | 38 -- .../sharedprefs/FloconSharedPrefsPlugin.kt | 20 - .../plugins/tables/FloconTablesPlugin.kt | 31 -- .../plugins/tables/builder/TableBuilder.kt | 25 -- FloconAndroid/flocon-no-op/build.gradle.kts | 2 +- FloconAndroid/flocon/build.gradle.kts | 6 +- .../io/github/openflocon/flocon/Flocon.kt | 4 +- .../flocon/FloconBroadcastReceiver.kt | 3 +- .../flocon/FloconContext.android.kt | 5 + .../openflocon/flocon/FloconCore.android.kt | 15 +- .../github/openflocon/flocon/FloconLogger.kt | 7 +- .../openflocon/flocon/ServerHost.android.kt | 2 +- .../flocon/core/AppInfos.android.kt | 5 +- .../FloconCrashReporterDataSource.android.kt | 60 +-- .../UncaughtExceptionHandler.android.kt | 23 -- .../database/FloconDatabasePlugin.android.kt | 273 +------------- .../device/FloconDevicePluginImpl.android.kt | 2 - .../plugins/device/GetAppIconUtils.android.kt | 66 +--- .../files/FloconFilesPlugin.android.kt | 100 +---- .../FloconNetworkPluginImpl.android.kt | 79 +--- .../FloconSharedPrefsPlugin.android.kt | 18 +- .../FloconCrashReporterDataSource.android.kt | 60 +++ .../UncaughtExceptionHandler.android.kt | 23 ++ .../database/FloconDatabasePlugin.android.kt | 277 ++++++++++++++ .../database/FloconSqliteDatabaseModel.kt | 16 +- .../device/FloconDevicePluginImpl.android.kt | 8 + .../device/GetAppIconUtils.android.kt | 69 ++++ .../files/FloconFilesPlugin.android.kt | 102 ++++++ .../FloconNetworkPluginImpl.android.kt | 88 +++++ .../sharedprefs/FloconSharedPreference.kt | 6 +- .../FloconSharedPrefsPlugin.android.kt | 25 ++ .../sharedprefs/SharedPreferencesFinder.kt | 16 +- .../flocon/utils/PlatformUtils.android.kt | 0 .../io/github/openflocon/flocon/Flocon.kt | 16 + .../io/github/openflocon/flocon/FloconApp.kt | 44 +++ .../openflocon/flocon/FloconConfiguration.kt | 12 +- .../github/openflocon/flocon/FloconContext.kt | 3 + .../io/github/openflocon/flocon/FloconCore.kt | 24 +- .../github/openflocon/flocon/FloconLogger.kt | 0 .../github/openflocon/flocon/FloconPlugin.kt | 5 +- .../flocon/client/FloconClientImpl.kt | 91 +---- .../openflocon/flocon/core/FloconPlugin.kt | 10 - .../analytics/FloconAnalyticsPlugin.kt | 14 +- .../analytics/mapper/AnalyticsItemsMapper.kt | 2 +- .../FloconCrashReporterPlugin.kt | 17 +- .../crashreporter/model/CrashReportMapper.kt | 1 - .../plugins/dashboard/FloconDashboardDSL.kt | 13 +- .../dashboard/FloconDashboardPlugin.kt | 7 +- .../plugins/dashboard/mapper/JsonMapper.kt | 26 +- .../plugins/database/FloconDatabasePlugin.kt | 28 +- .../deeplinks/FloconDeeplinksPlugin.kt | 66 ---- .../flocon/plugins/deeplinks/Mapping.kt | 53 --- .../deeplinks/model/DeeplinksRemote.kt | 65 ---- .../plugins/device/FloconDevicePluginImpl.kt | 5 +- .../flocon/plugins/files/FloconFilesPlugin.kt | 39 +- .../network/FloconNetworkPluginImpl.kt | 39 +- .../network/mapper/BadQualityToJson.kt | 5 +- .../mapper/FloconNetworkRequestToJson.kt | 10 +- .../network/mapper/MockResponseToJson.kt | 7 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 61 ++-- .../model/FloconPreferenceWrapper.kt | 1 + .../plugins/tables/FloconTablesPlugin.kt | 7 +- .../flocon/plugins/tables/model/TableItem.kt | 2 + .../analytics/FloconAnalyticsPlugin.kt | 39 ++ .../analytics/builder/AnalyticsBuilder.kt | 8 +- .../analytics/model/AnalyticsEvent.kt | 2 +- .../model/AnalyticsPropertiesConfig.kt | 2 +- .../pluginsold}/analytics/model/TableItem.kt | 2 +- .../FloconCrashReporterPlugin.kt | 16 + .../dashboard/FloconDashboardPlugin.kt | 15 + .../dashboard/builder/ContainerBuilder.kt | 14 + .../dashboard/builder/DashboardBuilder.kt | 8 +- .../dashboard/builder/FormBuilder.kt | 4 +- .../dashboard/builder/SectionBuilder.kt | 4 +- .../pluginsold/dashboard/dsl/ButtonDsl.kt | 19 + .../pluginsold}/dashboard/dsl/CheckBoxDsl.kt | 6 +- .../pluginsold}/dashboard/dsl/DashboardDsl.kt | 6 +- .../pluginsold}/dashboard/dsl/FormDsl.kt | 6 +- .../pluginsold/dashboard/dsl/HtmlDsl.kt | 9 + .../pluginsold/dashboard/dsl/MarkdownDsl.kt | 9 + .../pluginsold/dashboard/dsl/PlainTextDsl.kt | 26 ++ .../pluginsold/dashboard/dsl/SectionDsl.kt | 13 + .../pluginsold/dashboard/dsl/TextDsl.kt | 15 + .../pluginsold}/dashboard/dsl/TextField.kt | 6 +- .../dashboard/model/ContainerType.kt | 6 + .../pluginsold/dashboard/model/Dashboard.kt | 8 + .../dashboard/model/DashboardScope.kt | 10 +- .../dashboard/model/config/ButtonConfig.kt | 2 +- .../dashboard/model/config/CheckBoxConfig.kt | 2 +- .../dashboard/model/config/ContainerConfig.kt | 9 + .../dashboard/model/config/ElementConfig.kt | 3 + .../dashboard/model/config/FormConfig.kt | 4 +- .../dashboard/model/config/HtmlConfig.kt | 2 +- .../dashboard/model/config/LabelConfig.kt | 2 +- .../dashboard/model/config/MarkdownConfig.kt | 2 +- .../dashboard/model/config/PlainTextConfig.kt | 2 +- .../dashboard/model/config/SectionConfig.kt | 4 +- .../dashboard/model/config/TextConfig.kt | 2 +- .../dashboard/model/config/TextFieldConfig.kt | 2 +- .../database/FloconDatabasePlugin.kt | 34 ++ .../database/model/FloconDatabaseModel.kt | 2 +- .../pluginsold/device/FloconDevicePlugin.kt | 28 ++ .../pluginsold/files/FloconFilesPlugin.kt | 29 ++ .../pluginsold/network/FloconNetworkPlugin.kt | 45 +++ .../network/model/BadQualityConfig.kt | 2 +- .../network/model/FloconHttpRequest.kt | 2 +- .../network/model/FloconNetworkCallRequest.kt | 2 +- .../model/FloconNetworkCallResponse.kt | 2 +- .../network/model/FloconWebSocketEvent.kt | 2 +- .../model/FloconWebSocketMockListener.kt | 2 +- .../network/model/MockNetworkResponse.kt | 2 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 34 ++ .../buildFloconPreferencesDataSource.kt | 2 + .../sharedprefs/model/FloconPreference.kt | 4 +- .../model/FloconSharedPreferenceModel.kt | 5 + .../pluginsold/tables/FloconTablesPlugin.kt | 46 +++ .../pluginsold/tables/builder/TableBuilder.kt | 20 + .../tables/model/TableColumnConfig.kt | 2 +- .../pluginsold}/tables/model/TableItem.kt | 2 +- .../openflocon/flocon/utils/PlatformUtils.kt | 0 .../github/openflocon/flocon/FloconLogger.kt | 7 +- .../FloconSharedPrefsPlugin.ios.kt | 21 +- .../flocon/utils/PlatformUtils.ios.kt | 0 .../github/openflocon/flocon/FloconLogger.kt | 7 +- .../FloconSharedPrefsPlugin.jvm.kt | 19 +- .../flocon/utils/PlatformUtils.jvm.kt | 0 .../grpc-interceptor-base/build.gradle.kts | 2 +- .../openflocon/flocon/grpc/BadQuality.kt | 2 +- .../flocon/grpc/FloconGrpcBaseInterceptor.kt | 15 +- .../flocon/grpc/FloconGrpcPlugin.kt | 43 +-- .../flocon/grpc/model/RequestHolder.kt | 2 +- .../ktor-interceptor-no-op/build.gradle.kts | 2 +- .../ktor-interceptor/build.gradle.kts | 2 +- .../openflocon/flocon/ktor/BadQuality.kt | 2 +- .../flocon/ktor/FloconKtorPlugin.kt | 344 +++++++++--------- .../io/github/openflocon/flocon/ktor/Mocks.kt | 11 +- .../okhttp-interceptor/build.gradle.kts | 2 +- .../openflocon/flocon/okhttp/BadQuality.kt | 5 +- .../github/openflocon/flocon/okhttp/Mock.kt | 19 +- .../flocon/okhttp/OkHttpInterceptor.kt | 204 +++++------ .../okhttp/websocket/FloconWebSocket.kt | 36 +- .../flocon/myapplication/MainActivity.kt | 159 ++++---- .../dashboard/InitializeDashboard.kt | 18 +- .../myapplication/database/DogDatabase.kt | 24 +- .../database/InitializeDatabases.kt | 2 +- .../deeplinks/InitializeDeeplinks.kt | 31 -- .../sharedpreferences/Datastores.kt | 4 +- .../sharedpreferences/SharedPreferences.kt | 7 +- .../table/InitializeDashboard.kt | 13 +- .../sample-multiplatform/build.gradle.kts | 1 - .../myapplication/multi/MainActivity.kt | 5 - FloconAndroid/settings.gradle.kts | 4 +- 184 files changed, 2061 insertions(+), 2319 deletions(-) rename FloconAndroid/{flocon-base => deeplinks}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt (98%) delete mode 100644 FloconAndroid/flocon-base/.gitignore delete mode 100644 FloconAndroid/flocon-base/build.gradle.kts delete mode 100644 FloconAndroid/flocon-base/consumer-rules.pro delete mode 100644 FloconAndroid/flocon-base/proguard-rules.pro delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/ContainerBuilder.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/ButtonDsl.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/HtmlDsl.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/MarkdownDsl.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/PlainTextDsl.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/SectionDsl.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextDsl.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/ContainerType.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/Dashboard.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ContainerConfig.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ElementConfig.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt delete mode 100644 FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/builder/TableBuilder.kt create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconContext.android.kt rename FloconAndroid/{flocon-base => flocon}/src/androidMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt (97%) delete mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/UncaughtExceptionHandler.android.kt create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/UncaughtExceptionHandler.android.kt create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt rename FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/{plugins => pluginsold}/database/FloconSqliteDatabaseModel.kt (64%) create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePluginImpl.android.kt create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/device/GetAppIconUtils.android.kt create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPluginImpl.android.kt rename FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/{plugins => pluginsold}/sharedprefs/FloconSharedPreference.kt (90%) create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt rename FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/{plugins => pluginsold}/sharedprefs/SharedPreferencesFinder.kt (75%) rename FloconAndroid/{flocon-base => flocon}/src/androidMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.android.kt (100%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt rename FloconAndroid/{flocon-base => flocon}/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt (77%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconContext.kt rename FloconAndroid/{flocon-base => flocon}/src/commonMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt (100%) rename FloconAndroid/{flocon-base => flocon}/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt (87%) delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconPlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/analytics/builder/AnalyticsBuilder.kt (73%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/analytics/model/AnalyticsEvent.kt (80%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/analytics/model/AnalyticsPropertiesConfig.kt (76%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/analytics/model/TableItem.kt (74%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/FloconDashboardPlugin.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/ContainerBuilder.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/builder/DashboardBuilder.kt (53%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/builder/FormBuilder.kt (73%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/builder/SectionBuilder.kt (51%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/ButtonDsl.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/dsl/CheckBoxDsl.kt (57%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/dsl/DashboardDsl.kt (53%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/dsl/FormDsl.kt (61%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/HtmlDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/MarkdownDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/PlainTextDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/SectionDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/TextDsl.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/dsl/TextField.kt (62%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/ContainerType.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/Dashboard.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/DashboardScope.kt (72%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/ButtonConfig.kt (61%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/CheckBoxConfig.kt (68%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ContainerConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ElementConfig.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/FormConfig.kt (66%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/HtmlConfig.kt (55%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/LabelConfig.kt (55%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/MarkdownConfig.kt (56%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/PlainTextConfig.kt (65%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/SectionConfig.kt (57%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/TextConfig.kt (61%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/dashboard/model/config/TextFieldConfig.kt (72%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/database/model/FloconDatabaseModel.kt (75%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/network/model/BadQualityConfig.kt (97%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/network/model/FloconHttpRequest.kt (91%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/network/model/FloconNetworkCallRequest.kt (73%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/network/model/FloconNetworkCallResponse.kt (76%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/network/model/FloconWebSocketEvent.kt (87%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/network/model/FloconWebSocketMockListener.kt (56%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/network/model/MockNetworkResponse.kt (94%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/sharedprefs/model/FloconPreference.kt (82%) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/tables/model/TableColumnConfig.kt (75%) rename FloconAndroid/{flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold}/tables/model/TableItem.kt (68%) rename FloconAndroid/{flocon-base => flocon}/src/commonMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.kt (100%) rename FloconAndroid/{flocon-base => flocon}/src/iosMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt (97%) rename FloconAndroid/{flocon-base => flocon}/src/iosMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.ios.kt (100%) rename FloconAndroid/{flocon-base => flocon}/src/jvmMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt (97%) rename FloconAndroid/{flocon-base => flocon}/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.jvm.kt (100%) delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index 859f94eac..c3f4b30d0 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -41,7 +41,7 @@ kotlin { dependencies { - implementation(project(":flocon-base")) + implementation(project(":flocon")) implementation(platform(libs.kotlinx.coroutines.bom)) implementation(libs.jetbrains.kotlinx.coroutines.core) diff --git a/FloconAndroid/datastores-no-op/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt b/FloconAndroid/datastores-no-op/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt index 885a2fdf3..cfe707327 100644 --- a/FloconAndroid/datastores-no-op/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt +++ b/FloconAndroid/datastores-no-op/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt @@ -2,17 +2,8 @@ package io.github.openflocon.flocon.preferences.datastores.model import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.doublePreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.floatPreferencesKey -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreferenceValue -import kotlinx.coroutines.flow.first +import io.github.openflocon.flocon.`plugins-old`.sharedprefs.model.FloconPreference +import io.github.openflocon.flocon.`plugins-old`.sharedprefs.model.FloconPreferenceValue interface FloconDatastoreMapper { fun fromDatastore(datastoreValue: String) : String @@ -22,11 +13,11 @@ interface FloconDatastoreMapper { class FloconDatastorePreference( override val name: String, val dataStore: DataStore, -) : FloconPreference { +) : io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference { override suspend fun set( columnName: String, - value: FloconPreferenceValue + value: io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreferenceValue ) { // no op } @@ -35,7 +26,7 @@ class FloconDatastorePreference( return emptyList() // no op } - override suspend fun get(columnName: String): FloconPreferenceValue? { + override suspend fun get(columnName: String): io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreferenceValue? { return null // no op } diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 5f784f502..e30b81eca 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -41,7 +41,7 @@ kotlin { dependencies { - implementation(project(":flocon-base")) + implementation(project(":flocon")) implementation(platform(libs.kotlinx.coroutines.bom)) implementation(libs.jetbrains.kotlinx.coroutines.core) diff --git a/FloconAndroid/datastores/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt b/FloconAndroid/datastores/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt index a6b77fa6c..4e71bce4c 100644 --- a/FloconAndroid/datastores/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt +++ b/FloconAndroid/datastores/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt @@ -10,8 +10,8 @@ import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreferenceValue +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreferenceValue import kotlinx.coroutines.flow.first interface FloconDatastoreMapper { @@ -19,70 +19,72 @@ interface FloconDatastoreMapper { fun toDatastore(valueForDatastore: String) : String } -class FloconDatastorePreference( - override val name: String, - private val dataStore: DataStore, - private val mapper: FloconDatastoreMapper? = null // if we encrypted the datastore -) : FloconPreference { - - override suspend fun set( - columnName: String, - value: FloconPreferenceValue - ) { - try { - val data = dataStore.data.first().asMap() - val key = data.keys.find { it.name == columnName } ?: return - - dataStore.edit { - try { - when (it[key]) { - is String -> it[stringPreferencesKey(columnName)] = mapper?.let { - it.toDatastore(value.stringValue!!) - } ?: value.stringValue!! - is Int -> it[intPreferencesKey(columnName)] = value.intValue!! - is Boolean -> it[booleanPreferencesKey(columnName)] = value.booleanValue!! - is Float -> it[floatPreferencesKey(columnName)] = value.floatValue!! - is Long -> it[longPreferencesKey(columnName)] = value.longValue!! - is Double -> it[doublePreferencesKey(columnName)] = - value.floatValue!!.toDouble() - } - } catch (t: Throwable) { - FloconLogger.logError("cannot update datastore preference", t) - } - } - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "cannot edit datastore preference columns", t) - } - } - - override suspend fun columns(): List { - return try { - dataStore.data.first().asMap().map { it.key.name } - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "cannot get datastore preference columns", t) - emptyList() - } - } - - override suspend fun get(columnName: String): FloconPreferenceValue? { - return try { - val data = dataStore.data.first().asMap() - val key = data.keys.find { it.name == columnName } ?: return null - val value = data[key] ?: return null - - return when (value) { - is String -> FloconPreferenceValue(stringValue = mapper?.fromDatastore(value) ?: value) - is Int -> FloconPreferenceValue(intValue = value) - is Float -> FloconPreferenceValue(floatValue = value) - is Double -> FloconPreferenceValue(floatValue = value.toFloat()) - is Boolean -> FloconPreferenceValue(booleanValue = value) - is Long -> FloconPreferenceValue(longValue = value) - else -> null - } - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "cannot get datastore preference value", t) - null - } - } - -} \ No newline at end of file +//class FloconDatastorePreference( +// override val name: String, +// private val dataStore: DataStore, +// private val mapper: FloconDatastoreMapper? = null // if we encrypted the datastore +//) : FloconPreference { +// +// override suspend fun set( +// columnName: String, +// FloconPreferenceValue +// ) { +// try { +// val data = dataStore.data.first().asMap() +// val key = data.keys.find { it.name == columnName } ?: return +// +// dataStore.edit { +// try { +// when (it[key]) { +// is String -> it[stringPreferencesKey(columnName)] = mapper?.let { +// it.toDatastore(value.stringValue!!) +// } ?: value.stringValue!! +// is Int -> it[intPreferencesKey(columnName)] = value.intValue!! +// is Boolean -> it[booleanPreferencesKey(columnName)] = value.booleanValue!! +// is Float -> it[floatPreferencesKey(columnName)] = value.floatValue!! +// is Long -> it[longPreferencesKey(columnName)] = value.longValue!! +// is Double -> it[doublePreferencesKey(columnName)] = +// value.floatValue!!.toDouble() +// } +// } catch (t: Throwable) { +// FloconLogger.logError("cannot update datastore preference", t) +// } +// } +// } catch (t: Throwable) { +// FloconLogger.logError(t.message ?: "cannot edit datastore preference columns", t) +// } +// } +// +// override suspend fun columns(): List { +// return try { +// dataStore.data.first().asMap().map { it.key.name } +// } catch (t: Throwable) { +// FloconLogger.logError(t.message ?: "cannot get datastore preference columns", t) +// emptyList() +// } +// } +// +// override suspend fun get(columnName: String): io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreferenceValue? { +// return try { +// val data = dataStore.data.first().asMap() +// val key = data.keys.find { it.name == columnName } ?: return null +// val value = data[key] ?: return null +// +// return when (value) { +// is String -> FloconPreferenceValue( +// stringValue = mapper?.fromDatastore(value) ?: value +// ) +// is Int -> FloconPreferenceValue(intValue = value) +// is Float -> FloconPreferenceValue(floatValue = value) +// is Double -> FloconPreferenceValue(floatValue = value.toFloat()) +// is Boolean -> FloconPreferenceValue(booleanValue = value) +// is Long -> FloconPreferenceValue(longValue = value) +// else -> null +// } +// } catch (t: Throwable) { +// FloconLogger.logError(t.message ?: "cannot get datastore preference value", t) +// null +// } +// } +// +//} \ No newline at end of file diff --git a/FloconAndroid/deeplinks-no-op/build.gradle.kts b/FloconAndroid/deeplinks-no-op/build.gradle.kts index 1d0039cd9..b183bb71e 100644 --- a/FloconAndroid/deeplinks-no-op/build.gradle.kts +++ b/FloconAndroid/deeplinks-no-op/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon-base")) + implementation(project(":flocon")) implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) } } diff --git a/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt b/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt index a377c7896..83c349e12 100644 --- a/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt +++ b/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt @@ -1,7 +1,6 @@ package io.github.openflocon.flocon.plugins.deeplinks import io.github.openflocon.flocon.* -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel actual object FloconDeeplinks : FloconPluginFactory { override val name: String = "Deeplinks" diff --git a/FloconAndroid/deeplinks/build.gradle.kts b/FloconAndroid/deeplinks/build.gradle.kts index fc1029164..25714f431 100644 --- a/FloconAndroid/deeplinks/build.gradle.kts +++ b/FloconAndroid/deeplinks/build.gradle.kts @@ -23,7 +23,6 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon-base")) implementation(project(":flocon")) implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) implementation(libs.kotlinx.serialization.json) diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index 353080db9..3e2c697ea 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -24,6 +24,7 @@ object FloconDeeplinks : FloconPluginFactory?>(null) @@ -57,12 +58,3 @@ internal class FloconDeeplinksPluginImpl( } } } - -fun floconRegisterDeeplink(vararg deeplinks: String) { - val models = deeplinks.map { DeeplinkModel(link = it, parameters = emptyList()) } - FloconApp.instance?.client?.deeplinksPlugin?.registerDeeplinks(models) -} - -fun floconRegisterDeeplinks(deeplinks: List) { - FloconApp.instance?.client?.deeplinksPlugin?.registerDeeplinks(deeplinks) -} diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt similarity index 98% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt rename to FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index 316359861..1e4f57cbd 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.plugins.deeplinks -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel class DeeplinkLinkBuilder internal constructor( diff --git a/FloconAndroid/flocon-base/.gitignore b/FloconAndroid/flocon-base/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/FloconAndroid/flocon-base/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/FloconAndroid/flocon-base/build.gradle.kts b/FloconAndroid/flocon-base/build.gradle.kts deleted file mode 100644 index 4c63dfe76..000000000 --- a/FloconAndroid/flocon-base/build.gradle.kts +++ /dev/null @@ -1,118 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) -} - -kotlin { - androidTarget { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - - sourceSets { - val commonMain by getting { - dependencies { - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) - } - } - - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - } - } -} - -android { - namespace = "io.github.openflocon.flocon.base" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-base", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - pom { - name = "Flocon" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/consumer-rules.pro b/FloconAndroid/flocon-base/consumer-rules.pro deleted file mode 100644 index e69de29bb..000000000 diff --git a/FloconAndroid/flocon-base/proguard-rules.pro b/FloconAndroid/flocon-base/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/FloconAndroid/flocon-base/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt deleted file mode 100644 index b6df35c73..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt +++ /dev/null @@ -1,53 +0,0 @@ -package io.github.openflocon.flocon - -import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporterPlugin -import io.github.openflocon.flocon.plugins.dashboard.FloconDashboardPlugin -import io.github.openflocon.flocon.plugins.database.FloconDatabasePlugin -import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinksPlugin -import io.github.openflocon.flocon.plugins.device.FloconDevicePlugin -import io.github.openflocon.flocon.plugins.network.FloconNetworkPlugin -import io.github.openflocon.flocon.plugins.sharedprefs.FloconPreferencesPlugin -import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin -import kotlinx.coroutines.flow.StateFlow - -abstract class FloconApp { - lateinit var context: FloconContext - - companion object { - var instance: FloconApp? = null - private set - } - - interface Client { - - @Throws(Throwable::class) - suspend fun connect(onClosed: () -> Unit) - suspend fun disconnect() - - val databasePlugin: FloconDatabasePlugin? - val dashboardPlugin: FloconDashboardPlugin? - val tablePlugin: FloconTablePlugin? - val deeplinksPlugin: FloconDeeplinksPlugin? - val analyticsPlugin: FloconAnalyticsPlugin? - val networkPlugin: FloconNetworkPlugin? - val devicePlugin: FloconDevicePlugin? - val preferencesPlugin: FloconPreferencesPlugin? - val crashReporterPlugin: FloconCrashReporterPlugin? - - /** - * Retrieve a plugin instance by its [key]. - */ - fun getPlugin(key: FloconPluginKey<*, T>): T? - } - - open val client: Client? = null - - abstract val isInitialized : StateFlow - - protected fun initializeFlocon(context: FloconContext) { - this.context = context - instance = this - } - -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt deleted file mode 100644 index 846d49bf4..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ /dev/null @@ -1,38 +0,0 @@ -package io.github.openflocon.flocon.plugins.analytics - -import io.github.openflocon.flocon.* -import io.github.openflocon.flocon.plugins.analytics.builder.AnalyticsBuilder -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem - -class FloconAnalyticsConfig - -/** - * Flocon Analytics Plugin. - */ -expect object FloconAnalytics : FloconPluginFactory { - override fun createConfig(): FloconAnalyticsConfig - override fun install( - config: FloconAnalyticsConfig, - app: FloconApp - ): FloconAnalyticsPlugin - - override val name: String -} - -fun floconAnalytics(analyticsName: String) : AnalyticsBuilder { - return AnalyticsBuilder( - analyticsTableId = analyticsName, - analyticsPlugin = FloconApp.instance?.client?.analyticsPlugin, - ) -} - -fun FloconApp.analytics(analyticsName: String): AnalyticsBuilder { - return AnalyticsBuilder( - analyticsTableId = analyticsName, - analyticsPlugin = this.client?.analyticsPlugin, - ) -} - -interface FloconAnalyticsPlugin : FloconPlugin { - fun registerAnalytics(analyticsItems: List) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt deleted file mode 100644 index 3d1fa8dd9..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.openflocon.flocon.plugins.crashreporter - -import io.github.openflocon.flocon.* - -class FloconCrashReporterConfig { - var catchFatalErrors: Boolean = true -} - -/** - * Flocon Crash Reporter Plugin. - */ -expect object FloconCrashReporter : FloconPluginFactory - -interface FloconCrashReporterPlugin : FloconPlugin { - fun setupCrashHandler() -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt deleted file mode 100644 index bf2054d79..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard - -import io.github.openflocon.flocon.* -import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig - -class FloconDashboardConfig - -/** - * Flocon Dashboard Plugin. - */ -expect object FloconDashboard : FloconPluginFactory - -interface FloconDashboardPlugin : FloconPlugin { - fun registerDashboard(dashboardConfig: DashboardConfig) -} diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/ContainerBuilder.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/ContainerBuilder.kt deleted file mode 100644 index 3fbf6a10f..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/ContainerBuilder.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.builder - -import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig -import io.github.openflocon.flocon.plugins.dashboard.model.config.ElementConfig - -abstract class ContainerBuilder { - open val elements = mutableListOf() - - open fun add(element: ElementConfig) { - elements.add(element) - } - - abstract fun build(): ContainerConfig -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/ButtonDsl.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/ButtonDsl.kt deleted file mode 100644 index cf654088d..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/ButtonDsl.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl - -import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.config.ButtonConfig - -@DashboardDsl -fun ContainerBuilder.button( - text: String, - id : String, - onClick: () -> Unit, -) { - add( - ButtonConfig( - text = text, - id = id, - onClick = onClick, - ) - ) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/HtmlDsl.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/HtmlDsl.kt deleted file mode 100644 index 5b93e4884..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/HtmlDsl.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl - -import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.config.HtmlConfig - -@DashboardDsl -fun ContainerBuilder.html(label: String, value: String) { - add(HtmlConfig(label = label, value = value)) -} diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/MarkdownDsl.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/MarkdownDsl.kt deleted file mode 100644 index 1a9d5f6f3..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/MarkdownDsl.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl - -import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.config.MarkdownConfig - -@DashboardDsl -fun ContainerBuilder.markdown(label: String, value: String) { - add(MarkdownConfig(label = label, value = value)) -} diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/PlainTextDsl.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/PlainTextDsl.kt deleted file mode 100644 index 343788f69..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/PlainTextDsl.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl - -import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.config.PlainTextConfig - -@DashboardDsl -fun ContainerBuilder.plainText(label: String, value: String) { - add( - PlainTextConfig( - label = label, - value = value, - type = "text", - ) - ) -} - -@DashboardDsl -fun ContainerBuilder.json(label: String, value: String) { - add(PlainTextConfig( - label = label, - value = value, - type = "json", - )) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/SectionDsl.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/SectionDsl.kt deleted file mode 100644 index 4e4ef095e..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/SectionDsl.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl - -import io.github.openflocon.flocon.plugins.dashboard.builder.DashboardBuilder -import io.github.openflocon.flocon.plugins.dashboard.builder.SectionBuilder - -@DashboardDsl -fun DashboardBuilder.section(name: String, block: SectionBuilder.() -> Unit) { - val builder = SectionBuilder(name).apply { - block() - } - - add(builder.build()) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextDsl.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextDsl.kt deleted file mode 100644 index 9a80dde6d..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextDsl.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl - -import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.config.TextConfig -import io.github.openflocon.flocon.plugins.dashboard.model.config.LabelConfig - -@DashboardDsl -fun ContainerBuilder.text(label: String, value: String, color: Int? = null) { - add(TextConfig(label = label, value = value, color = color)) -} - -@DashboardDsl -fun ContainerBuilder.label(label: String, color: Int? = null) { - add(LabelConfig(label = label, color = color)) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/ContainerType.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/ContainerType.kt deleted file mode 100644 index e33e10bf2..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/ContainerType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.model - -enum class ContainerType { - FORM, - SECTION -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/Dashboard.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/Dashboard.kt deleted file mode 100644 index 5deacba9b..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/Dashboard.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.model - -import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig - -data class DashboardConfig( - val id: String, - val containers: List -) \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ContainerConfig.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ContainerConfig.kt deleted file mode 100644 index 904b76885..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ContainerConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config - -import io.github.openflocon.flocon.plugins.dashboard.model.ContainerType - -sealed interface ContainerConfig { - val name: String - val elements: List - val containerType: ContainerType -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ElementConfig.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ElementConfig.kt deleted file mode 100644 index 68d654c92..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ElementConfig.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config - -sealed interface ElementConfig \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt deleted file mode 100644 index 06b7acc6c..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt +++ /dev/null @@ -1,41 +0,0 @@ -package io.github.openflocon.flocon.plugins.database - -import io.github.openflocon.flocon.* -import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.FloconFileDatabaseModel - -class FloconDatabaseConfig - -/** - * Flocon Database Plugin. - * Used to inspect Room or other SQL databases. - */ -expect object FloconDatabase : FloconPluginFactory - -fun floconRegisterDatabase(database: FloconDatabaseModel) { - FloconApp.instance?.client?.databasePlugin?.register( - database - ) -} - -fun floconRegisterDatabase(displayName: String, absolutePath: String) { - floconRegisterDatabase( - FloconFileDatabaseModel( - displayName = displayName, - absolutePath = absolutePath, - ) - ) -} - -fun floconLogDatabaseQuery(dbName: String, sqlQuery: String, bindArgs: List) { - FloconApp.instance?.client?.databasePlugin?.logQuery( - dbName = dbName, - sqlQuery = sqlQuery, - bindArgs = bindArgs, - ) -} - -interface FloconDatabasePlugin : FloconPlugin { - fun register(floconDatabaseModel: FloconDatabaseModel) - fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt deleted file mode 100644 index 29533742c..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.openflocon.flocon.plugins.deeplinks.model - -data class DeeplinkModel( - val link: String, - val label: String? = null, - val description: String? = null, - val parameters: List, -) { - - sealed interface Parameter { - val paramName: String - - data class AutoComplete( - override val paramName: String, - val autoComplete: List - ) : Parameter - - data class Variable( - override val paramName: String, - val variableName: String - ) : Parameter - - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt deleted file mode 100644 index c099eaa66..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.openflocon.flocon.plugins.device - -import io.github.openflocon.flocon.* - -class FloconDeviceConfig - -/** - * Flocon Device Plugin. - */ -expect object FloconDevice : FloconPluginFactory - -interface FloconDevicePlugin : FloconPlugin { - fun registerWithSerial(serial: String) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt deleted file mode 100644 index 3645820c5..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.openflocon.flocon.plugins.files - -import io.github.openflocon.flocon.* - -class FloconFilesConfig { - val roots = mutableListOf() -} - -/** - * Flocon Files Plugin. - * Used to inspect and download files from the device. - */ -expect object FloconFiles : FloconPluginFactory - -interface FloconFilesPlugin : FloconPlugin \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt deleted file mode 100644 index aaaf34432..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt +++ /dev/null @@ -1,38 +0,0 @@ -package io.github.openflocon.flocon.plugins.network - -import io.github.openflocon.flocon.* -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.FloconWebSocketEvent -import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketMockListener -import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse - -class FloconNetworkConfig { - var badQualityConfig: BadQualityConfig? = null - val mocks = mutableListOf() -} - -/** - * Flocon Network Plugin. - * Used to inspect HTTP/S and WebSocket calls. - */ -expect object FloconNetwork : FloconPluginFactory - -fun floconLogWebSocketEvent(event: FloconWebSocketEvent) { - FloconApp.instance?.client?.networkPlugin?.logWebSocket(event) -} - -interface FloconNetworkPlugin : FloconPlugin { - val mocks: Collection - val badQualityConfig: BadQualityConfig? - - fun logRequest(request: FloconNetworkCallRequest) - fun logResponse(response: FloconNetworkCallResponse) - - fun logWebSocket( - event: FloconWebSocketEvent, - ) - - fun registerWebSocketMockListener(id: String, listener: FloconWebSocketMockListener) -} diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt deleted file mode 100644 index 416f2cad5..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.github.openflocon.flocon.plugins.sharedprefs - -import io.github.openflocon.flocon.* -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconSharedPreferenceModel - -class FloconPreferencesConfig - -/** - * Flocon Preferences Plugin. - * Used to inspect SharedPreferences or other key-value stores. - */ -expect object FloconPreferences : FloconPluginFactory - -fun floconRegisterSharedPreference(sharedPreference: FloconSharedPreferenceModel) { - FloconApp.instance?.client?.preferencesPlugin?.register(sharedPreference) -} - -interface FloconPreferencesPlugin : FloconPlugin { - fun register(sharedPreference: FloconSharedPreferenceModel) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt deleted file mode 100644 index ad5d0ec9e..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.openflocon.flocon.plugins.tables - -import io.github.openflocon.flocon.* -import io.github.openflocon.flocon.plugins.tables.builder.TableBuilder -import io.github.openflocon.flocon.plugins.tables.model.TableItem - -class FloconTableConfig - -/** - * Flocon Table Plugin. - * Used to display custom data tables. - */ -expect object FloconTable : FloconPluginFactory - -fun floconTable(tableName: String) : TableBuilder { - return TableBuilder( - tableId = tableName, - tablePlugin = FloconApp.instance?.client?.tablePlugin, - ) -} - -fun FloconApp.table(tableName: String): TableBuilder { - return TableBuilder( - tableId = tableName, - tablePlugin = this.client?.tablePlugin, - ) -} - -interface FloconTablePlugin : FloconPlugin { - fun registerItems(tableItems: List) -} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/builder/TableBuilder.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/builder/TableBuilder.kt deleted file mode 100644 index 8e7f22a26..000000000 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/builder/TableBuilder.kt +++ /dev/null @@ -1,25 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package io.github.openflocon.flocon.plugins.tables.builder - -import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin -import io.github.openflocon.flocon.plugins.tables.model.TableColumnConfig -import io.github.openflocon.flocon.plugins.tables.model.TableItem -import io.github.openflocon.flocon.utils.currentTimeMillis -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid - -class TableBuilder( - val tableName: String, - private val tablePlugin: FloconTablePlugin?, -) { - fun log(vararg columns: TableColumnConfig) { - val dashboardConfig = TableItem( - id = Uuid.random().toString(), - name = tableName, - columns = columns.toList(), - createdAt = currentTimeMillis(), - ) - tablePlugin?.registerTable(dashboardConfig) - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index 963d4a6d6..895bd36bf 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -23,7 +23,7 @@ kotlin { val commonMain by getting { dependencies { implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) - api(project(":flocon-base")) + api(project(":flocon")) } } diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index c887d14c2..457438cef 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -21,13 +21,15 @@ kotlin { iosArm64() iosSimulatorArm64() + compilerOptions { + freeCompilerArgs.add("-XXLanguage:+ExpectRefinement") + } + sourceSets { val commonMain by getting { dependencies { implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) implementation(libs.kotlinx.serialization.json) - api(project(":flocon-base")) - api(project(":deeplinks")) } } diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.kt index 85a2d610d..9f47da0fc 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -3,11 +3,13 @@ package io.github.openflocon.flocon import android.content.Context object Flocon : FloconCore() { + fun initialize(context: Context, block: FloconConfiguration.() -> Unit = {}) { val configuration = FloconConfiguration().apply(block) super.initializeFlocon( - context = FloconContext(appContext = context), + context = FloconContext(context = context), configuration = configuration ) } + } diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconBroadcastReceiver.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconBroadcastReceiver.kt index 8949202b1..4b70f8283 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconBroadcastReceiver.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconBroadcastReceiver.kt @@ -3,7 +3,6 @@ package io.github.openflocon.flocon import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.util.Log internal class FloconBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -13,7 +12,7 @@ internal class FloconBroadcastReceiver : BroadcastReceiver() { if (serial != null) { FloconLogger.log("serial : $serial") - Flocon.client?.devicePlugin?.registerWithSerial(serial) + //Flocon.client?.devicePlugin?.registerWithSerial(serial) } } } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconContext.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconContext.android.kt new file mode 100644 index 000000000..4d7ce3ef7 --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconContext.android.kt @@ -0,0 +1,5 @@ +package io.github.openflocon.flocon + +import android.content.Context + +actual class FloconContext(val context: Context) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconCore.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconCore.android.kt index fe2393a4b..59b7f7117 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconCore.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconCore.android.kt @@ -1,19 +1,18 @@ package io.github.openflocon.flocon -import android.content.Context import android.util.Log import android.widget.Toast -actual class FloconContext(val appContext: Context) - internal actual fun displayClearTextError(context: FloconContext) { Toast.makeText( - context.appContext, - "Cannot start Flocon : ClearText Issue, see Logcat", - Toast.LENGTH_LONG - ).show() + /* context = */ context.context, + /* text = */ "Cannot start Flocon : ClearText Issue, see Logcat", + /* duration = */ Toast.LENGTH_LONG + ) + .show() Log.e( - "Flocon", + /* tag = */ "Flocon", + /* msg = */ "Flocon uses ClearText communication to the server, it seems you already have a network-security-config setup on your project, please ensure you allowed cleartext communication on your debug app https://github.com/openflocon/Flocon?tab=readme-ov-file#-why-flocon-cant-see-your-device-calls-and-how-to-fix-it-" ) } \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/androidMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt similarity index 97% rename from FloconAndroid/flocon-base/src/androidMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt rename to FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt index aa34a812e..68fc4f94a 100644 --- a/FloconAndroid/flocon-base/src/androidMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt @@ -5,17 +5,16 @@ import android.util.Log actual object FloconLogger { actual var enabled = false private const val TAG = "FloconLogger" - + actual fun logError(text: String, throwable: Throwable?) { if(enabled) { Log.e(TAG, text, throwable) } } - + actual fun log(text: String) { if(enabled) { Log.d(TAG, text) } } -} - +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/ServerHost.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/ServerHost.android.kt index 45b760333..87275d6d9 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/ServerHost.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/ServerHost.android.kt @@ -3,6 +3,6 @@ package io.github.openflocon.flocon import io.github.openflocon.flocon.utils.NetUtils internal actual fun getServerHost(floconContext: FloconContext): String { - val appContext = floconContext.appContext + val appContext = floconContext.context return NetUtils.getServerHost(context = appContext) } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/core/AppInfos.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/core/AppInfos.android.kt index 45bfbe9bf..7405f5d02 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/core/AppInfos.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/core/AppInfos.android.kt @@ -18,9 +18,10 @@ private fun deviceName(): String { } internal actual fun getAppInfos(floconContext: FloconContext): AppInfos { - val appContext = floconContext.appContext + val appContext = floconContext.context + return AppInfos( - deviceId = deviceId(floconContext.appContext), + deviceId = deviceId(appContext), deviceName = deviceName(), appName = AppUtils.getAppName(appContext), appPackageName = AppUtils.getAppPackageName(appContext), diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.android.kt index 1c6b0d62e..e736d8c8b 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.android.kt @@ -1,59 +1,13 @@ package io.github.openflocon.flocon.plugins.crashreporter -import android.content.Context import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel -import io.github.openflocon.flocon.plugins.crashreporter.model.crashReportFromJson -import io.github.openflocon.flocon.plugins.crashreporter.model.toJson -import java.io.File - -internal class FloconCrashReporterDataSourceAndroid( - private val context: Context -) : FloconCrashReporterDataSource { - - private val crashesDir = File(context.filesDir, "flocon_crashes") - - init { - crashesDir.mkdirs() - } - - override fun saveCrash(crash: CrashReportDataModel) { - try { - val file = File(crashesDir, "${crash.crashId}.json") - val jsonString = crash.toJson() - file.writeText(jsonString) - } catch (t: Throwable) { - FloconLogger.logError("Error saving crash", t) - } - } - - override fun loadPendingCrashes(): List { - return try { - crashesDir.listFiles() - ?.mapNotNull { file -> - try { - crashReportFromJson(file.readText()) - } catch (t: Throwable) { - t.printStackTrace() - null - } - } ?: emptyList() - } catch (t: Throwable) { - FloconLogger.logError("Error loading pending crashes", t) - emptyList() - } - } - - override fun deleteCrash(crashId: String) { - try { - File(crashesDir, "$crashId.json").delete() - } catch (t: Throwable) { - FloconLogger.logError("Failed to delete crash report: $crashId.json", t) - } - } -} internal actual fun buildFloconCrashReporterDataSource(context: FloconContext): FloconCrashReporterDataSource { - return FloconCrashReporterDataSourceAndroid(context.appContext) + TODO("Not yet implemented") } + +internal actual fun setupUncaughtExceptionHandler( + context: FloconContext, + onCrash: (Throwable) -> Unit +) { +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/UncaughtExceptionHandler.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/UncaughtExceptionHandler.android.kt deleted file mode 100644 index 45c238648..000000000 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/UncaughtExceptionHandler.android.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.openflocon.flocon.plugins.crashreporter - -import android.os.Build -import io.github.openflocon.flocon.FloconContext - -internal actual fun setupUncaughtExceptionHandler( - context: FloconContext, - onCrash: (Throwable) -> Unit -) { - val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() - - Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> - try { - // Save crash - onCrash(throwable) - } catch (t: Throwable) { - t.printStackTrace() - } finally { - // Call original handler to let the app crash normally - defaultHandler?.uncaughtException(thread, throwable) - } - } -} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.android.kt index 08023c639..8a4ac38fb 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.android.kt @@ -1,276 +1,7 @@ package io.github.openflocon.flocon.plugins.database -import android.content.Context -import android.database.Cursor -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.FloconFileDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel -import java.io.File -import java.util.Locale internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { - return FloconDatabaseDataSourceAndroid(context.appContext) -} - -internal class FloconDatabaseDataSourceAndroid(private val context: Context) : - FloconDatabaseDataSource { - - private val MAX_DEPTH = 7 - - override fun executeSQL( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteSqlResponse { - val databaseModel = registeredDatabases.find { it.displayName == databaseName } - return when(databaseModel) { - is FloconSqliteDatabaseModel -> { - executeSQL( - database = databaseModel.database, - query = query, - ) - } - else -> openDbAndExecuteQuery( - databaseName = databaseName, - query = query, - ) - } - } - - private fun openDbAndExecuteQuery( - databaseName: String, - query: String - ): DatabaseExecuteSqlResponse { - var helper: SupportSQLiteOpenHelper? = null - return try { - val path = context.getDatabasePath(databaseName) - val version = getDatabaseVersion(path = path.absolutePath) - helper = FrameworkSQLiteOpenHelperFactory().create( - SupportSQLiteOpenHelper.Configuration.builder(context) - .name(path.absolutePath) - .callback(object : SupportSQLiteOpenHelper.Callback(version) { - override fun onCreate(db: SupportSQLiteDatabase) { - // no op - } - - override fun onUpgrade( - db: SupportSQLiteDatabase, - oldVersion: Int, - newVersion: Int - ) { - // no op - } - }) - .build() - ) - val database = helper.writableDatabase - - executeSQL( - database = database, - query = query, - ) - } catch (t: Throwable) { - DatabaseExecuteSqlResponse.Error( - message = t.message ?: "error on executeSQL", - originalSql = query, - ) - } finally { - helper?.close() - } - } - - private fun executeSQL( - database: SupportSQLiteDatabase, - query: String - ): DatabaseExecuteSqlResponse { - return try { - val firstWordUpperCase = getFirstWord(query).uppercase(Locale.getDefault()) - when (firstWordUpperCase) { - "UPDATE", "DELETE" -> executeUpdateDelete(database, query) - "INSERT" -> executeInsert(database, query) - "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(database, query) - else -> executeRawQuery(database, query) - } - } catch (t: Throwable) { - DatabaseExecuteSqlResponse.Error( - message = t.message ?: "error on executeSQL", - originalSql = query, - ) - } - } - - override fun getAllDataBases( - registeredDatabases: List - ): List { - val databasesDir = context.getDatabasePath("dummy_db").parentFile ?: return emptyList() - - val foundDatabases = mutableListOf() - // Start the recursive search from the base databases directory - scanDirectoryForDatabases( - directory = databasesDir, - depth = 0, - foundDatabases = foundDatabases - ) - - registeredDatabases.forEach { - when(it) { - is FloconFileDatabaseModel -> { - // check if file exists here - if (File(it.absolutePath).exists()) { - foundDatabases.add( - DeviceDataBaseDataModel( - id = it.absolutePath, - name = it.displayName, - ) - ) - } - } - else -> { - foundDatabases.add( - DeviceDataBaseDataModel( - id = it.displayName, - name = it.displayName, - ) - ) - } - } - } - - return foundDatabases - } - - /** - * Recursively scans a directory for SQLite database files. - * - * @param directory The current directory to scan. - * @param foundDatabases The mutable list to add found databases to. - */ - private fun scanDirectoryForDatabases( - directory: File, - depth: Int, - foundDatabases: MutableList - ) { - if (depth >= MAX_DEPTH) { - return; - } - directory.listFiles()?.forEach { file -> - if (file.isDirectory) { - // If it's a directory, recursively call this function - scanDirectoryForDatabases( - directory = file, - depth = depth + 1, - foundDatabases = foundDatabases, - ) - } else { - // If it's a file, check if it's a database file - if (file.isFile && - !file.name.endsWith("-wal") && // Write-Ahead Log - !file.name.endsWith("-shm") && // Shared-Memory - !file.name.endsWith("-journal") // Older journaling mode - ) { - foundDatabases.add( - DeviceDataBaseDataModel( - id = file.absolutePath, // Use absolute path for unique ID - name = file.name, - ) - ) - } - } - } - } -} - - -private fun executeSelect( - database: SupportSQLiteDatabase, - query: String, -): DatabaseExecuteSqlResponse { - val cursor: Cursor = database.query(query) - try { - val columnNames = cursor.columnNames.toList() - val rows = cursorToList(cursor) - return DatabaseExecuteSqlResponse.Select( - columns = columnNames, - values = rows, - ) - } finally { - cursor.close() - } -} - -private fun executeUpdateDelete( - database: SupportSQLiteDatabase, - query: String, -): DatabaseExecuteSqlResponse { - val statement = database.compileStatement(query) - val count: Int = statement.executeUpdateDelete() - return DatabaseExecuteSqlResponse.UpdateDelete(count) -} - -private fun executeInsert( - database: SupportSQLiteDatabase, - query: String, -): DatabaseExecuteSqlResponse { - val statement = database.compileStatement(query) - val insertedId: Long = statement.executeInsert() - return DatabaseExecuteSqlResponse.Insert(insertedId) -} - -private fun executeRawQuery( - database: SupportSQLiteDatabase, - query: String, -): DatabaseExecuteSqlResponse { - database.execSQL(query) - return DatabaseExecuteSqlResponse.RawSuccess -} - -private fun getFirstWord(s: String): String { - var s = s - s = s.trim { it <= ' ' } - val firstSpace = s.indexOf(' ') - return if (firstSpace >= 0) s.substring(0, firstSpace) else s -} - -private fun cursorToList(cursor: Cursor): List> { - val rows = mutableListOf>() - val numColumns = cursor.columnCount - while (cursor.moveToNext()) { - val values = mutableListOf() - for (column in 0.. null - Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(column).toString() - Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(column).toString() - Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(column).toString() - Cursor.FIELD_TYPE_STRING -> cursor.getString(column).toString() - else -> cursor.getString(column) - } -} - -// must use the old way to get the version... -private fun getDatabaseVersion( - path: String, -): Int { - return android.database.sqlite.SQLiteDatabase.openDatabase( - path, - null, - android.database.sqlite.SQLiteDatabase.OPEN_READONLY - ).use { db -> - db.rawQuery("PRAGMA user_version", null).use { cursor -> - if (cursor.moveToFirst()) cursor.getInt(0) else 0 - } - } -} + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt index 14ce93b10..d7c3cc2ce 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt @@ -1,8 +1,6 @@ package io.github.openflocon.flocon.plugins.device -import com.jakewharton.processphoenix.ProcessPhoenix import io.github.openflocon.flocon.FloconContext internal actual fun restartApp(context: FloconContext) { - ProcessPhoenix.triggerRebirth(context.appContext) } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt index 501e3c1dc..09e6ec220 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt @@ -1,69 +1,7 @@ package io.github.openflocon.flocon.plugins.device -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.drawable.Drawable -import android.util.Base64 import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import java.io.ByteArrayOutputStream actual fun getAppIconBase64(context: FloconContext): String? { - return getAppIconBase64(context.appContext) -} - - -internal fun getAppIconBase64(context: Context): String? { - return try { - val bitmap = getAppIcon(context) - val resizedBitmap = resizeAppIcon(bitmap, maxSize = 300) - encodeToBase64(resizedBitmap) - } catch (t: Throwable) { - FloconLogger.logError( - text = "Error while getting app icon", - throwable = t, - ) - null - } -} - -private fun getAppIcon(context: Context): Bitmap { - // 1. Récupération de l'icône en bitmap - val packageManager = context.packageManager - val packageName = context.packageName - val iconDrawable: Drawable = packageManager.getApplicationIcon(packageName) - - val bitmap = Bitmap.createBitmap( - iconDrawable.intrinsicWidth, - iconDrawable.intrinsicHeight, - Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmap) - iconDrawable.setBounds(0, 0, canvas.width, canvas.height) - iconDrawable.draw(canvas) - - return bitmap -} - -private fun encodeToBase64(resizedBitmap: Bitmap): String { - // 3. Conversion en Base64 - val outputStream = ByteArrayOutputStream() - resizedBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) - val byteArray = outputStream.toByteArray() - - return Base64.encodeToString(byteArray, Base64.NO_WRAP) // NO_WRAP pour éviter les \n -} - -private fun resizeAppIcon(bitmap: Bitmap, maxSize: Int): Bitmap { - val width = bitmap.width - val height = bitmap.height - val scale = minOf(maxSize.toFloat() / width, maxSize.toFloat() / height, 1f) - - return Bitmap.createScaledBitmap( - bitmap, - (width * scale).toInt(), - (height * scale).toInt(), - true - ) -} + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt index 840f97db4..f6e708b5c 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt @@ -1,101 +1,7 @@ package io.github.openflocon.flocon.plugins.files import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconFile -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.plugins.files.model.fromdevice.FileDataModel -import java.io.File -internal class FileDataSourceAndroid( - private val context: FloconContext, -) : FileDataSource { - override fun getFile( - path: String, - isConstantPath: Boolean - ): FloconFile? { - val file = if (isConstantPath) { - when (path) { - "caches" -> context.appContext.cacheDir - "files" -> context.appContext.filesDir - else -> File(path) - } - } else { - File(path) - } - return file.takeIf { it.exists() }?.let { FloconFile(it) } - } - - override fun getFolderContent( - path: String, - isConstantPath: Boolean, - withFoldersSize: Boolean, - ): List { - val directory = getFile(path = path, isConstantPath = isConstantPath) - - val directoryFile = directory?.file - if (directoryFile == null || !directoryFile.exists() || !directoryFile.isDirectory) { - FloconLogger.logError("file '$path' does not exists.", throwable = null) - return emptyList() - } - - val childrenFiles = directoryFile.listFiles() ?: return emptyList() - - return childrenFiles.map { file -> - FileDataModel( - name = file.name, - isDirectory = file.isDirectory, - path = file.absolutePath, - size = if (file.isFile) file.length() else getFolderSize(file, withFoldersSize), - lastModified = file.lastModified() - ) - } - } - - private fun getFolderSize(file: File, withFoldersSize: Boolean): Long { - return if (withFoldersSize) { - file.walk() - .filter { it.isFile } - .map { it.length() } - .sum() - } else 0L - } - - - override fun deleteFile(path: String) { - deleteInternal(path) - } - - override fun deleteFiles(path: List) { - path.forEach { - deleteInternal(it) - } - } - - private fun deleteInternal(path: String) { - deleteInternal(File(path)) - } - - private fun deleteInternal(file: File) { - try { - if (!file.exists()) return - - file.deleteRecursively() - } catch (t: Throwable) { - t.printStackTrace() - } - } - - override fun deleteFolderContent(folder: FloconFile) { - deleteFolderContent(folder.file) - } - - private fun deleteFolderContent(folder: File) { - if (folder.isDirectory) { - folder.listFiles()?.forEach { file -> - deleteInternal(file) - } - } - } -} - -internal actual fun fileDataSource(context: FloconContext) : FileDataSource = FileDataSourceAndroid(context) \ No newline at end of file +internal actual fun fileDataSource(context: FloconContext): FileDataSource { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.android.kt index 254934ef4..9c6088f1e 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.android.kt @@ -1,84 +1,7 @@ package io.github.openflocon.flocon.plugins.network -import android.content.Context import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -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.toJsonString -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.MockNetworkResponse -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { - return FloconNetworkDataSourceAndroid( - context = context.appContext, - ) -} - -internal class FloconNetworkDataSourceAndroid(private val context: Context) : FloconNetworkDataSource { - override fun saveMocksToFile(mocks: List) { - try { - val file = File(context.filesDir, FLOCON_NETWORK_MOCKS_JSON) - val jsonString = writeMockResponsesToJson(mocks = mocks) - FileOutputStream(file).use { - it.write(jsonString.toByteArray()) - } - } catch (t: Throwable) { - FloconLogger.logError("issue in saveMocksToFile", t) - } - } - - override fun loadMocksFromFile(): List { - return try { - val file = File(context.filesDir, FLOCON_NETWORK_MOCKS_JSON) - if (!file.exists()) { - return emptyList() - } - - val jsonString = FileInputStream(file).use { - it.readBytes().toString(Charsets.UTF_8) - } - parseMockResponses(jsonString = jsonString) - } catch (t: Throwable) { - FloconLogger.logError("issue in loadMocksFromFile", t) - emptyList() - } - } - - override 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 = jsonString) - } catch (t: Throwable) { - FloconLogger.logError("issue in loadBadNetworkConfig", t) - null - } - } - - override fun saveBadNetworkConfig(config: BadQualityConfig?) { - try { - val file = File(context.filesDir, FLOCON_NETWORK_BAD_CONFIG_JSON) - if (config == null) { - file.delete() - } else { - val jsonString = config.toJsonString() - FileOutputStream(file).use { - it.write(jsonString.toByteArray()) - } - } - } catch (t: Throwable) { - FloconLogger.logError("issue in saveBadNetworkConfig", t) - } - } + TODO("Not yet implemented") } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt index 08b75dbec..cc2ee1768 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt @@ -1,21 +1,7 @@ package io.github.openflocon.flocon.plugins.sharedprefs -import android.content.Context import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference -// Got some code from Flipper client -// https://github.com/facebook/flipper/blob/main/android/src/main/java/com/facebook/flipper/plugins/sharedpreferences/SharedPreferencesFlipperPlugin.java - -internal class FloconPreferencesDataSourceAndroid( - private val context: Context, -) : FloconPreferencesDataSource { - - override fun detectLocalPreferences(): List { - return SharedPreferencesFinder.buildDescriptorForAllPrefsFiles(context) - } -} - -internal actual fun buildFloconPreferencesDataSource(context: FloconContext): FloconPreferencesDataSource { - return FloconPreferencesDataSourceAndroid(context = context.appContext) +internal actual fun buildFloconSharedPreferenceDataSource(context: FloconContext): FloconSharedPreferenceDataSource { + TODO("Not yet implemented") } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt new file mode 100644 index 000000000..f7e9a720a --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt @@ -0,0 +1,60 @@ +package io.github.openflocon.flocon.pluginsold.crashreporter + +import android.content.Context +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporterDataSource +import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel +import io.github.openflocon.flocon.plugins.crashreporter.model.crashReportFromJson +import io.github.openflocon.flocon.plugins.crashreporter.model.toJson +import java.io.File + +internal class FloconCrashReporterDataSourceAndroid( + private val context: Context +) : FloconCrashReporterDataSource { + + private val crashesDir = File(context.filesDir, "flocon_crashes") + + init { + crashesDir.mkdirs() + } + + override fun saveCrash(crash: CrashReportDataModel) { + try { + val file = File(crashesDir, "${crash.crashId}.json") + val jsonString = crash.toJson() + file.writeText(jsonString) + } catch (t: Throwable) { + FloconLogger.logError("Error saving crash", t) + } + } + + override fun loadPendingCrashes(): List { + return try { + crashesDir.listFiles() + ?.mapNotNull { file -> + try { + crashReportFromJson(file.readText()) + } catch (t: Throwable) { + t.printStackTrace() + null + } + } ?: emptyList() + } catch (t: Throwable) { + FloconLogger.logError("Error loading pending crashes", t) + emptyList() + } + } + + override fun deleteCrash(crashId: String) { + try { + File(crashesDir, "$crashId.json").delete() + } catch (t: Throwable) { + FloconLogger.logError("Failed to delete crash report: $crashId.json", t) + } + } +} + +//internal actual fun buildFloconCrashReporterDataSource(context: FloconContext): FloconCrashReporterDataSource { +// return FloconCrashReporterDataSourceAndroid(context.context) +//} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/UncaughtExceptionHandler.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/UncaughtExceptionHandler.android.kt new file mode 100644 index 000000000..1608a2c24 --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/UncaughtExceptionHandler.android.kt @@ -0,0 +1,23 @@ +package io.github.openflocon.flocon.pluginsold.crashreporter + +import android.os.Build +import io.github.openflocon.flocon.FloconContext + +//internal actual fun setupUncaughtExceptionHandler( +// context: FloconContext, +// onCrash: (Throwable) -> Unit +//) { +// val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() +// +// Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> +// try { +// // Save crash +// onCrash(throwable) +// } catch (t: Throwable) { +// t.printStackTrace() +// } finally { +// // Call original handler to let the app crash normally +// defaultHandler?.uncaughtException(thread, throwable) +// } +// } +//} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt new file mode 100644 index 000000000..ec7c7ba2f --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt @@ -0,0 +1,277 @@ +package io.github.openflocon.flocon.pluginsold.database + +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.plugins.database.FloconDatabaseDataSource +import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel +import io.github.openflocon.flocon.pluginsold.database.model.FloconDatabaseModel +import java.io.File +import java.util.Locale + +//internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { +// return FloconDatabaseDataSourceAndroid(context.context) +//} + +internal class FloconDatabaseDataSourceAndroid(private val context: Context) : + FloconDatabaseDataSource { + + private val MAX_DEPTH = 7 + + override fun executeSQL( + registeredDatabases: List, + databaseName: String, + query: String + ): DatabaseExecuteSqlResponse { + val databaseModel = registeredDatabases.find { it.displayName == databaseName } + return when(databaseModel) { + is FloconSqliteDatabaseModel -> { + executeSQL( + database = databaseModel.database, + query = query, + ) + } + else -> openDbAndExecuteQuery( + databaseName = databaseName, + query = query, + ) + } + } + + private fun openDbAndExecuteQuery( + databaseName: String, + query: String + ): DatabaseExecuteSqlResponse { + var helper: SupportSQLiteOpenHelper? = null + return try { + val path = context.getDatabasePath(databaseName) + val version = getDatabaseVersion(path = path.absolutePath) + helper = FrameworkSQLiteOpenHelperFactory().create( + SupportSQLiteOpenHelper.Configuration.builder(context) + .name(path.absolutePath) + .callback(object : SupportSQLiteOpenHelper.Callback(version) { + override fun onCreate(db: SupportSQLiteDatabase) { + // no op + } + + override fun onUpgrade( + db: SupportSQLiteDatabase, + oldVersion: Int, + newVersion: Int + ) { + // no op + } + }) + .build() + ) + val database = helper.writableDatabase + + executeSQL( + database = database, + query = query, + ) + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "error on executeSQL", + originalSql = query, + ) + } finally { + helper?.close() + } + } + + private fun executeSQL( + database: SupportSQLiteDatabase, + query: String + ): DatabaseExecuteSqlResponse { + return try { + val firstWordUpperCase = getFirstWord(query).uppercase(Locale.getDefault()) + when (firstWordUpperCase) { + "UPDATE", "DELETE" -> executeUpdateDelete(database, query) + "INSERT" -> executeInsert(database, query) + "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(database, query) + else -> executeRawQuery(database, query) + } + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "error on executeSQL", + originalSql = query, + ) + } + } + + override fun getAllDataBases( + registeredDatabases: List + ): List { + val databasesDir = context.getDatabasePath("dummy_db").parentFile ?: return emptyList() + + val foundDatabases = mutableListOf() + // Start the recursive search from the base databases directory + scanDirectoryForDatabases( + directory = databasesDir, + depth = 0, + foundDatabases = foundDatabases + ) + +// registeredDatabases.forEach { +// when(it) { +// is FloconFileDatabaseModel -> { +// // check if file exists here +// if (File(it.absolutePath).exists()) { +// foundDatabases.add( +// DeviceDataBaseDataModel( +// id = it.absolutePath, +// name = it.displayName, +// ) +// ) +// } +// } +// else -> { +// foundDatabases.add( +// DeviceDataBaseDataModel( +// id = it.displayName, +// name = it.displayName, +// ) +// ) +// } +// } +// } + + return foundDatabases + } + + /** + * Recursively scans a directory for SQLite database files. + * + * @param directory The current directory to scan. + * @param foundDatabases The mutable list to add found databases to. + */ + private fun scanDirectoryForDatabases( + directory: File, + depth: Int, + foundDatabases: MutableList + ) { + if (depth >= MAX_DEPTH) { + return; + } + directory.listFiles()?.forEach { file -> + if (file.isDirectory) { + // If it's a directory, recursively call this function + scanDirectoryForDatabases( + directory = file, + depth = depth + 1, + foundDatabases = foundDatabases, + ) + } else { + // If it's a file, check if it's a database file + if (file.isFile && + !file.name.endsWith("-wal") && // Write-Ahead Log + !file.name.endsWith("-shm") && // Shared-Memory + !file.name.endsWith("-journal") // Older journaling mode + ) { + foundDatabases.add( + DeviceDataBaseDataModel( + id = file.absolutePath, // Use absolute path for unique ID + name = file.name, + ) + ) + } + } + } + } +} + + +private fun executeSelect( + database: SupportSQLiteDatabase, + query: String, +): DatabaseExecuteSqlResponse { + val cursor: Cursor = database.query(query) + try { + val columnNames = cursor.columnNames.toList() + val rows = cursorToList(cursor) + return DatabaseExecuteSqlResponse.Select( + columns = columnNames, + values = rows, + ) + } finally { + cursor.close() + } +} + +private fun executeUpdateDelete( + database: SupportSQLiteDatabase, + query: String, +): DatabaseExecuteSqlResponse { + val statement = database.compileStatement(query) + val count: Int = statement.executeUpdateDelete() + return DatabaseExecuteSqlResponse.UpdateDelete(count) +} + +private fun executeInsert( + database: SupportSQLiteDatabase, + query: String, +): DatabaseExecuteSqlResponse { + val statement = database.compileStatement(query) + val insertedId: Long = statement.executeInsert() + return DatabaseExecuteSqlResponse.Insert(insertedId) +} + +private fun executeRawQuery( + database: SupportSQLiteDatabase, + query: String, +): DatabaseExecuteSqlResponse { + database.execSQL(query) + return DatabaseExecuteSqlResponse.RawSuccess +} + +private fun getFirstWord(s: String): String { + var s = s + s = s.trim { it <= ' ' } + val firstSpace = s.indexOf(' ') + return if (firstSpace >= 0) s.substring(0, firstSpace) else s +} + +private fun cursorToList(cursor: Cursor): List> { + val rows = mutableListOf>() + val numColumns = cursor.columnCount + while (cursor.moveToNext()) { + val values = mutableListOf() + for (column in 0.. null + Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(column).toString() + Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(column).toString() + Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(column).toString() + Cursor.FIELD_TYPE_STRING -> cursor.getString(column).toString() + else -> cursor.getString(column) + } +} + +// must use the old way to get the version... +private fun getDatabaseVersion( + path: String, +): Int { + return SQLiteDatabase.openDatabase( + path, + null, + SQLiteDatabase.OPEN_READONLY + ).use { db -> + db.rawQuery("PRAGMA user_version", null).use { cursor -> + if (cursor.moveToFirst()) cursor.getInt(0) else 0 + } + } +} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconSqliteDatabaseModel.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconSqliteDatabaseModel.kt similarity index 64% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconSqliteDatabaseModel.kt rename to FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconSqliteDatabaseModel.kt index 229ab494d..b5b0087b9 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconSqliteDatabaseModel.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconSqliteDatabaseModel.kt @@ -1,8 +1,8 @@ -package io.github.openflocon.flocon.plugins.database +package io.github.openflocon.flocon.pluginsold.database import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper -import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel +import io.github.openflocon.flocon.pluginsold.database.model.FloconDatabaseModel internal class FloconSqliteDatabaseModel( override val displayName: String, @@ -10,12 +10,12 @@ internal class FloconSqliteDatabaseModel( ) : FloconDatabaseModel fun floconRegisterDatabase(displayName: String, database: SupportSQLiteDatabase) { - floconRegisterDatabase( - FloconSqliteDatabaseModel( - displayName = displayName, - database = database, - ) - ) +// floconRegisterDatabase( +// FloconSqliteDatabaseModel( +// displayName = displayName, +// database = database, +// ) +// ) } diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePluginImpl.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePluginImpl.android.kt new file mode 100644 index 000000000..88f1f8466 --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePluginImpl.android.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocon.pluginsold.device + +import com.jakewharton.processphoenix.ProcessPhoenix +import io.github.openflocon.flocon.FloconContext + +//internal actual fun restartApp(context: FloconContext) { +// ProcessPhoenix.triggerRebirth(context.context) +//} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/device/GetAppIconUtils.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/device/GetAppIconUtils.android.kt new file mode 100644 index 000000000..7de9ea904 --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/device/GetAppIconUtils.android.kt @@ -0,0 +1,69 @@ +package io.github.openflocon.flocon.pluginsold.device + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.Base64 +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import java.io.ByteArrayOutputStream + +//actual fun getAppIconBase64(context: FloconContext): String? { +// return getAppIconBase64(context.context) +//} + + +internal fun getAppIconBase64(context: Context): String? { + return try { + val bitmap = getAppIcon(context) + val resizedBitmap = resizeAppIcon(bitmap, maxSize = 300) + encodeToBase64(resizedBitmap) + } catch (t: Throwable) { + FloconLogger.logError( + text = "Error while getting app icon", + throwable = t, + ) + null + } +} + +private fun getAppIcon(context: Context): Bitmap { + // 1. Récupération de l'icône en bitmap + val packageManager = context.packageManager + val packageName = context.packageName + val iconDrawable: Drawable = packageManager.getApplicationIcon(packageName) + + val bitmap = Bitmap.createBitmap( + iconDrawable.intrinsicWidth, + iconDrawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + iconDrawable.setBounds(0, 0, canvas.width, canvas.height) + iconDrawable.draw(canvas) + + return bitmap +} + +private fun encodeToBase64(resizedBitmap: Bitmap): String { + // 3. Conversion en Base64 + val outputStream = ByteArrayOutputStream() + resizedBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + + return Base64.encodeToString(byteArray, Base64.NO_WRAP) // NO_WRAP pour éviter les \n +} + +private fun resizeAppIcon(bitmap: Bitmap, maxSize: Int): Bitmap { + val width = bitmap.width + val height = bitmap.height + val scale = minOf(maxSize.toFloat() / width, maxSize.toFloat() / height, 1f) + + return Bitmap.createScaledBitmap( + bitmap, + (width * scale).toInt(), + (height * scale).toInt(), + true + ) +} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt new file mode 100644 index 000000000..5d8ad7a43 --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt @@ -0,0 +1,102 @@ +package io.github.openflocon.flocon.pluginsold.files + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.plugins.files.FileDataSource +import io.github.openflocon.flocon.plugins.files.model.fromdevice.FileDataModel +import java.io.File + +internal class FileDataSourceAndroid( + private val context: FloconContext, +) : FileDataSource { + override fun getFile( + path: String, + isConstantPath: Boolean + ): FloconFile? { + val file = if (isConstantPath) { + when (path) { + "caches" -> context.context.cacheDir + "files" -> context.context.filesDir + else -> File(path) + } + } else { + File(path) + } + return file.takeIf { it.exists() }?.let { FloconFile(it) } + } + + override fun getFolderContent( + path: String, + isConstantPath: Boolean, + withFoldersSize: Boolean, + ): List { + val directory = getFile(path = path, isConstantPath = isConstantPath) + + val directoryFile = directory?.file + if (directoryFile == null || !directoryFile.exists() || !directoryFile.isDirectory) { + FloconLogger.logError("file '$path' does not exists.", throwable = null) + return emptyList() + } + + val childrenFiles = directoryFile.listFiles() ?: return emptyList() + + return childrenFiles.map { file -> + FileDataModel( + name = file.name, + isDirectory = file.isDirectory, + path = file.absolutePath, + size = if (file.isFile) file.length() else getFolderSize(file, withFoldersSize), + lastModified = file.lastModified() + ) + } + } + + private fun getFolderSize(file: File, withFoldersSize: Boolean): Long { + return if (withFoldersSize) { + file.walk() + .filter { it.isFile } + .map { it.length() } + .sum() + } else 0L + } + + + override fun deleteFile(path: String) { + deleteInternal(path) + } + + override fun deleteFiles(path: List) { + path.forEach { + deleteInternal(it) + } + } + + private fun deleteInternal(path: String) { + deleteInternal(File(path)) + } + + private fun deleteInternal(file: File) { + try { + if (!file.exists()) return + + file.deleteRecursively() + } catch (t: Throwable) { + t.printStackTrace() + } + } + + override fun deleteFolderContent(folder: FloconFile) { + deleteFolderContent(folder.file) + } + + private fun deleteFolderContent(folder: File) { + if (folder.isDirectory) { + folder.listFiles()?.forEach { file -> + deleteInternal(file) + } + } + } +} + +//internal actual fun fileDataSource(context: FloconContext) : FileDataSource = FileDataSourceAndroid(context) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPluginImpl.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPluginImpl.android.kt new file mode 100644 index 000000000..0b7493ae0 --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPluginImpl.android.kt @@ -0,0 +1,88 @@ +package io.github.openflocon.flocon.pluginsold.network + +import android.content.Context +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.plugins.network.FLOCON_NETWORK_BAD_CONFIG_JSON +import io.github.openflocon.flocon.plugins.network.FLOCON_NETWORK_MOCKS_JSON +import io.github.openflocon.flocon.plugins.network.FloconNetworkDataSource +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.toJsonString +import io.github.openflocon.flocon.plugins.network.mapper.writeMockResponsesToJson +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + +//internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { +// return FloconNetworkDataSourceAndroid( +// context = context.context, +// ) +//} + +internal class FloconNetworkDataSourceAndroid(private val context: Context) : + FloconNetworkDataSource { + override fun saveMocksToFile(mocks: List) { + try { + val file = File(context.filesDir, FLOCON_NETWORK_MOCKS_JSON) + val jsonString = writeMockResponsesToJson(mocks = mocks) + FileOutputStream(file).use { + it.write(jsonString.toByteArray()) + } + } catch (t: Throwable) { + FloconLogger.logError("issue in saveMocksToFile", t) + } + } + + override fun loadMocksFromFile(): List { + return try { + val file = File(context.filesDir, FLOCON_NETWORK_MOCKS_JSON) + if (!file.exists()) { + return emptyList() + } + + val jsonString = FileInputStream(file).use { + it.readBytes().toString(Charsets.UTF_8) + } + parseMockResponses(jsonString = jsonString) + } catch (t: Throwable) { + FloconLogger.logError("issue in loadMocksFromFile", t) + emptyList() + } + } + + override 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 = jsonString) + } catch (t: Throwable) { + FloconLogger.logError("issue in loadBadNetworkConfig", t) + null + } + } + + override fun saveBadNetworkConfig(config: BadQualityConfig?) { + try { + val file = File(context.filesDir, FLOCON_NETWORK_BAD_CONFIG_JSON) + if (config == null) { + file.delete() + } else { + val jsonString = config.toJsonString() + 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/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPreference.kt similarity index 90% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt rename to FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPreference.kt index 1bbe8d4ac..fd54d1dab 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPreference.kt @@ -1,8 +1,8 @@ -package io.github.openflocon.flocon.plugins.sharedprefs +package io.github.openflocon.flocon.pluginsold.sharedprefs import android.content.SharedPreferences -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreferenceValue +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreferenceValue data class FloconSharedPreference( override val name: String, diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt new file mode 100644 index 000000000..9661da305 --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt @@ -0,0 +1,25 @@ +package io.github.openflocon.flocon.pluginsold.sharedprefs + +import android.content.Context +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference + +// Got some code from Flipper client +// https://github.com/facebook/flipper/blob/main/android/src/main/java/com/facebook/flipper/plugins/sharedpreferences/SharedPreferencesFlipperPlugin.java + +internal class FloconPreferencesDataSourceAndroid( + private val context: Context, +) : FloconPreferencesDataSource { + + override fun detectLocalPreferences(): List { + return SharedPreferencesFinder.buildDescriptorForAllPrefsFiles(context) + } +} + +interface FloconPreferencesDataSource { + fun detectLocalPreferences(): List +} + +//internal actual fun buildFloconPreferencesDataSource(context: FloconContext): FloconPreferencesDataSource { +// return FloconPreferencesDataSourceAndroid(context = context.context) +//} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/SharedPreferencesFinder.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/SharedPreferencesFinder.kt similarity index 75% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/SharedPreferencesFinder.kt rename to FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/SharedPreferencesFinder.kt index eef8b5068..b25ec6260 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/SharedPreferencesFinder.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/SharedPreferencesFinder.kt @@ -1,10 +1,10 @@ -package io.github.openflocon.flocon.plugins.sharedprefs +package io.github.openflocon.flocon.pluginsold.sharedprefs import android.content.Context import android.content.Context.MODE_PRIVATE import android.os.Build import android.preference.PreferenceManager -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference import java.io.File internal object SharedPreferencesFinder { @@ -23,8 +23,11 @@ internal object SharedPreferencesFinder { for (each in list) { val prefName = each.substring(0, each.indexOf(".xml")) descriptors.add( - FloconSharedPreference(prefName, sharedPreferences = context.getSharedPreferences(prefName, MODE_PRIVATE) - )) + FloconSharedPreference( + prefName, + sharedPreferences = context.getSharedPreferences(prefName, MODE_PRIVATE) + ) + ) } } @@ -32,7 +35,10 @@ internal object SharedPreferencesFinder { descriptors.add( FloconSharedPreference( name = defaultSharedPrefName, - sharedPreferences = context.getSharedPreferences(defaultSharedPrefName, MODE_PRIVATE) + sharedPreferences = context.getSharedPreferences( + defaultSharedPrefName, + MODE_PRIVATE + ) ) ) diff --git a/FloconAndroid/flocon-base/src/androidMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.android.kt similarity index 100% rename from FloconAndroid/flocon-base/src/androidMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.android.kt rename to FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.android.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt new file mode 100644 index 000000000..411529ea7 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -0,0 +1,16 @@ +package io.github.openflocon.flocon + +import io.github.openflocon.flocon.client.FloconClientImpl +// +//internal class Flocon( +// private val context: FloconContext, +// private val plugins: List +//) { +// +// private val client = FloconClientImpl( +// context = context, +// configuration = FloconConfiguration(), +// plugins = plugins +// ) +// +//} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt new file mode 100644 index 000000000..4319f3dab --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt @@ -0,0 +1,44 @@ +package io.github.openflocon.flocon + +import kotlinx.coroutines.flow.StateFlow + +abstract class FloconApp { + lateinit var context: FloconContext + + companion object { + var instance: FloconApp? = null + private set + } + + interface Client { + + @Throws(Throwable::class) + suspend fun connect(onClosed: () -> Unit) + suspend fun disconnect() + +// val databasePlugin: FloconDatabasePlugin? +// val dashboardPlugin: FloconDashboardPlugin? +// val tablePlugin: FloconTablePlugin? + //val deeplinksPlugin: FloconDeeplinksPlugin? +// val analyticsPlugin: FloconAnalyticsPlugin? +// val networkPlugin: FloconNetworkPlugin? +// val devicePlugin: FloconDevicePlugin? +// val preferencesPlugin: FloconPreferencesPlugin? +// val crashReporterPlugin: FloconCrashReporterPlugin? + + /** + * Retrieve a plugin instance by its [key]. + */ + fun getPlugin(key: String): T? + } + + open val client: Client? = null + + abstract val isInitialized: StateFlow + +// protected fun initializeFlocon(context: FloconContext) { +// this.context = context +// instance = this +// } + +} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt similarity index 77% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index b22025e11..993c9dce3 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -1,10 +1,7 @@ package io.github.openflocon.flocon -/** - * Configuration builder for Flocon SDK. - * Used in [Flocon.initialize] to configure the SDK and install plugins. - */ class FloconConfiguration internal constructor() { + internal val pluginConfigs = mutableMapOf, Any>() /** @@ -18,10 +15,11 @@ class FloconConfiguration internal constructor() { config.configure() pluginConfigs[factory] = config } + } -fun flocon(block: FloconConfiguration.() -> Unit) { +fun startFlocon(context: FloconContext, block: FloconConfiguration.() -> Unit) { val configuration = FloconConfiguration().apply(block) - - + + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconContext.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconContext.kt new file mode 100644 index 000000000..153093b48 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconContext.kt @@ -0,0 +1,3 @@ +package io.github.openflocon.flocon + +expect class FloconContext \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt index f9bf17c23..871f74f61 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt @@ -32,26 +32,24 @@ abstract class FloconCore : FloconApp() { context: FloconContext, configuration: FloconConfiguration = FloconConfiguration() ) { - super.initializeFlocon(context) - val newClient = FloconClientImpl(context, configuration) - _client = newClient + //super.initializeFlocon(context) + // val newClient = FloconClientImpl(context, configuration) + //_client = newClient // Setup crash handler early to catch crashes during initialization - newClient.crashReporterPlugin?.setupCrashHandler() + //newClient.crashReporterPlugin?.setupCrashHandler() _isInitialized.value = true - scope.launch { - start( - client = newClient, - context = context - ) - } - - super.initializeFlocon() +// scope.launch { +// start( +// client = newClient, +// context = context +// ) +// } } - private suspend fun start(client: FloconApp.Client, context: FloconContext) { + private suspend fun start(client: Client, context: FloconContext) { // try to connect, it fail : try again in 3s try { client.connect( diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt similarity index 100% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt similarity index 87% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt index 4771c2713..3a05b5b0b 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -5,10 +5,13 @@ package io.github.openflocon.flocon * Plugins can receive messages from the server and react to connection events. */ interface FloconPlugin { + val key: String + fun onMessageReceived( method: String, body: String, ) + fun onConnectedToServer() } @@ -31,7 +34,7 @@ interface FloconPluginFactory : FloconPlugin fun createConfig(): Config /** - * Install the plugin into the [FloconApp] instance with the given [config]. + * Install the plugin into the [io.github.openflocon.flocon.FloconApp] instance with the given [config]. */ fun install(config: Config, app: FloconApp): PluginInstance } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt index 9778ea7e3..1fe2a7159 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt @@ -3,26 +3,6 @@ package io.github.openflocon.flocon.client import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.* import io.github.openflocon.flocon.model.* -import io.github.openflocon.flocon.plugins.analytics.FloconAnalytics -import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporter -import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporterPlugin -import io.github.openflocon.flocon.plugins.dashboard.FloconDashboard -import io.github.openflocon.flocon.plugins.dashboard.FloconDashboardPlugin -import io.github.openflocon.flocon.plugins.database.FloconDatabase -import io.github.openflocon.flocon.plugins.database.FloconDatabasePlugin -import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks -import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinksPlugin -import io.github.openflocon.flocon.plugins.device.FloconDevice -import io.github.openflocon.flocon.plugins.device.FloconDevicePlugin -import io.github.openflocon.flocon.plugins.files.FloconFiles -import io.github.openflocon.flocon.plugins.files.FloconFilesPlugin -import io.github.openflocon.flocon.plugins.network.FloconNetwork -import io.github.openflocon.flocon.plugins.network.FloconNetworkPlugin -import io.github.openflocon.flocon.plugins.sharedprefs.FloconPreferences -import io.github.openflocon.flocon.plugins.sharedprefs.FloconPreferencesPlugin -import io.github.openflocon.flocon.plugins.tables.FloconTable -import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin import io.github.openflocon.flocon.utils.currentTimeMillis import io.github.openflocon.flocon.websocket.FloconHttpClient import io.github.openflocon.flocon.websocket.FloconWebSocketClient @@ -36,65 +16,29 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch internal class FloconClientImpl( - private val appContext: FloconContext, + private val context: FloconContext, private val configuration: FloconConfiguration, + private val plugins: List ) : FloconApp.Client, FloconMessageSender, FloconFileSender { private val FLOCON_WEBSOCKET_PORT = 9023 private val FLOCON_HTTP_PORT = 9024 - private val appInstance by lazy { - currentTimeMillis() - } - - private val appInfos by lazy { - getAppInfos(appContext) - } - - private val versionName by lazy { - BuildConfig.APP_VERSION - } + private val appInstance by lazy { currentTimeMillis() } + private val appInfos by lazy { getAppInfos(context) } + private val versionName by lazy { BuildConfig.APP_VERSION } + private val address by lazy { getServerHost(context) } private val webSocketClient: FloconWebSocketClient = buildFloconWebSocketClient() private val httpClient: FloconHttpClient = buildFloconHttpClient() - private val address by lazy { - getServerHost(appContext) - } private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val installedPlugins = mutableMapOf, Any>() - private val pluginIdToPlugin = mutableMapOf() - init { - configuration.pluginConfigs.forEach { (factory, config) -> - @Suppress("UNCHECKED_CAST") - val plugin = (factory as FloconPluginFactory).install(config, FloconApp.instance!!) - installedPlugins[factory] = plugin - factory.pluginId?.let { id -> - if (plugin is FloconPlugin) { - pluginIdToPlugin[id] = plugin - } - } - } + override fun getPlugin(key: String): T? { + return plugins.find { it.key == key } as? T } - override fun getPlugin(key: FloconPluginKey<*, T>): T? { - return installedPlugins[key] as? T - } - - // region plugins backward compatibility - override val databasePlugin: FloconDatabasePlugin? get() = getPlugin(FloconDatabase) - override val dashboardPlugin: FloconDashboardPlugin? get() = getPlugin(FloconDashboard) - override val tablePlugin: FloconTablePlugin? get() = getPlugin(FloconTable) - override val deeplinksPlugin: FloconDeeplinksPlugin? get() = getPlugin(FloconDeeplinks) - override val analyticsPlugin: FloconAnalyticsPlugin? get() = getPlugin(FloconAnalytics) - override val networkPlugin: FloconNetworkPlugin? get() = getPlugin(FloconNetwork) - override val devicePlugin: FloconDevicePlugin? get() = getPlugin(FloconDevice) - override val preferencesPlugin: FloconPreferencesPlugin? get() = getPlugin(FloconPreferences) - override val crashReporterPlugin: FloconCrashReporterPlugin? get() = getPlugin(FloconCrashReporter) - // endregion - @Throws(Throwable::class) override suspend fun connect( onClosed: () -> Unit, @@ -105,11 +49,7 @@ internal class FloconClientImpl( onMessageReceived = ::onMessageReceived, onClosed = onClosed, ) - installedPlugins.values.forEach { - if (it is FloconPlugin) { - it.onConnectedToServer() - } - } + plugins.forEach(FloconPlugin::onConnectedToServer) } override suspend fun disconnect() { @@ -119,10 +59,12 @@ internal class FloconClientImpl( private fun onMessageReceived(message: String) { coroutineScope.launch(Dispatchers.IO) { floconMessageFromServerFromJson(message)?.let { messageFromServer -> - pluginIdToPlugin[messageFromServer.plugin]?.onMessageReceived( - method = messageFromServer.method, - body = messageFromServer.body, - ) + messageFromServer.plugin + plugins.find { it.key == messageFromServer.plugin } + ?.onMessageReceived( + method = messageFromServer.method, + body = messageFromServer.body, + ) } } } @@ -145,7 +87,8 @@ internal class FloconClientImpl( appInstance = appInstance, platform = appInfos.platform, versionName = versionName, - ).toFloconMessageToServer(), + ) + .toFloconMessageToServer(), ) } } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconPlugin.kt deleted file mode 100644 index dd50d5c15..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconPlugin.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.openflocon.flocon.core - -import io.github.openflocon.flocon.model.FloconMessageFromServer - -internal interface FloconPlugin { - fun onMessageReceived( - messageFromServer: FloconMessageFromServer, - ) - fun onConnectedToServer() -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index 4ffa613ed..91e19704f 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -1,11 +1,17 @@ package io.github.openflocon.flocon.plugins.analytics -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem import io.github.openflocon.flocon.plugins.analytics.mapper.analyticsItemsToJson +import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsConfig +import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsPlugin +import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem -actual object FloconAnalytics : FloconPluginFactory { +object FloconAnalytics : FloconPluginFactory { override val name: String = "Analytics" override val pluginId: String = Protocol.ToDevice.Analytics.Plugin override fun createConfig() = FloconAnalyticsConfig() @@ -19,6 +25,8 @@ actual object FloconAnalytics : FloconPluginFactory { +object FloconCrashReporter : + FloconPluginFactory { override val name: String = "CrashReporter" - override val pluginId: String = Protocol.ToDevice.Analytics.Plugin // Crash reporter is usually write-only but we can set an ID + override val pluginId: String = + Protocol.ToDevice.Analytics.Plugin // Crash reporter is usually write-only but we can set an ID + override fun createConfig() = FloconCrashReporterConfig() - override fun install(config: FloconCrashReporterConfig, app: FloconApp): FloconCrashReporterPlugin { + override fun install( + config: FloconCrashReporterConfig, + app: FloconApp + ): FloconCrashReporterPlugin { val client = app.client as FloconMessageSender return FloconCrashReporterPluginImpl( - context = FloconContext(appContext = null), // Handled by datasource + context = TODO(), //FloconContext(appContext = null), // Handled by datasource sender = client, coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), ) @@ -32,6 +40,7 @@ internal class FloconCrashReporterPluginImpl( private var sender: FloconMessageSender, private val coroutineScope: CoroutineScope, ) : FloconPlugin, FloconCrashReporterPlugin { + override val key: String = "CRASH_REPORTER" private val dataSource = buildFloconCrashReporterDataSource(context) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportMapper.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportMapper.kt index bc944130d..dec1f3fb1 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportMapper.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportMapper.kt @@ -3,7 +3,6 @@ package io.github.openflocon.flocon.plugins.crashreporter.model import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json internal fun CrashReportDataModel.toJson(): String { return FloconEncoder.json.encodeToString(this) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardDSL.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardDSL.kt index 5560f448d..3c9bc8353 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardDSL.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardDSL.kt @@ -1,11 +1,10 @@ package io.github.openflocon.flocon.plugins.dashboard -import io.github.openflocon.flocon.FloconApp -import io.github.openflocon.flocon.plugins.dashboard.builder.FormBuilder -import io.github.openflocon.flocon.plugins.dashboard.builder.SectionBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig -import io.github.openflocon.flocon.plugins.dashboard.model.DashboardScope -import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig +import io.github.openflocon.flocon.pluginsold.dashboard.builder.FormBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.builder.SectionBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig +import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardScope +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ContainerConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob @@ -41,7 +40,7 @@ fun CoroutineScope.floconDashboard(id: String, block: DashboardScope.() -> Unit) sectionsFlow.collect { containers -> val config = DashboardConfig(id = id, containers = containers) - FloconApp.instance?.client?.dashboardPlugin?.registerDashboard(config) + //FloconApp.instance?.client?.dashboardPlugin?.registerDashboard(config) } } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt index c4fe95c04..7059b5063 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt @@ -4,16 +4,18 @@ import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.plugins.dashboard.mapper.toJson import io.github.openflocon.flocon.plugins.dashboard.model.DashboardCallback -import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig import io.github.openflocon.flocon.plugins.dashboard.model.todevice.ToDeviceCheckBoxValueChangedMessage import io.github.openflocon.flocon.plugins.dashboard.model.todevice.ToDeviceSubmittedFormMessage import io.github.openflocon.flocon.plugins.dashboard.model.todevice.ToDeviceSubmittedTextFieldMessage +import io.github.openflocon.flocon.pluginsold.dashboard.FloconDashboardConfig +import io.github.openflocon.flocon.pluginsold.dashboard.FloconDashboardPlugin +import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -actual object FloconDashboard : FloconPluginFactory { + object FloconDashboard : FloconPluginFactory { override val name: String = "Dashboard" override val pluginId: String = Protocol.ToDevice.Dashboard.Plugin override fun createConfig() = FloconDashboardConfig() @@ -27,6 +29,7 @@ actual object FloconDashboard : FloconPluginFactory { +object FloconDatabase : FloconPluginFactory { override val name: String = "Database" override val pluginId: String = Protocol.ToDevice.Database.Plugin override fun createConfig() = FloconDatabaseConfig() override fun install(config: FloconDatabaseConfig, app: FloconApp): FloconDatabasePlugin { return FloconDatabasePluginImpl( sender = app.client as FloconMessageSender, - context = FloconContext(appContext = null), // Handled by actual buildFloconDatabaseDataSource + context = TODO() // FloconContext(appContext = null), // Handled by actual buildFloconDatabaseDataSource ) } } @@ -45,6 +50,7 @@ internal class FloconDatabasePluginImpl( private var sender: FloconMessageSender, private val context: FloconContext, ) : FloconPlugin, FloconDatabasePlugin { + override val key: String = "DATABASE" private val registeredDatabases = MutableStateFlow>(emptyList()) @@ -102,9 +108,13 @@ internal class FloconDatabasePluginImpl( } } +// override fun register(floconDatabaseModel: FloconDatabaseModel) { +// registeredDatabases.update { it + floconDatabaseModel } +// sendAllDatabases(sender) +// } + override fun register(floconDatabaseModel: FloconDatabaseModel) { - registeredDatabases.update { it + floconDatabaseModel } - sendAllDatabases(sender) + TODO("Not yet implemented") } override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt deleted file mode 100644 index 9df144790..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ /dev/null @@ -1,66 +0,0 @@ -package io.github.openflocon.flocon.plugins.deeplinks - -import io.github.openflocon.flocon.* -import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel -import io.github.openflocon.flocon.plugins.deeplinks.mapper.toDeeplinksJson -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update - -actual object FloconDeeplinks : FloconPluginFactory { - override val name: String = "Deeplinks" - override val pluginId: String = Protocol.ToDevice.Deeplink.Plugin - override fun createConfig() = FloconDeeplinksConfig() - override fun install(config: FloconDeeplinksConfig, app: FloconApp): FloconDeeplinksPlugin { - val plugin = FloconDeeplinksPluginImpl( - sender = app.client as FloconMessageSender - ) - if (config.deeplinks.isNotEmpty()) { - plugin.registerDeeplinks(config.deeplinks) - } - return plugin - } -} - -internal class FloconDeeplinksPluginImpl( - private val sender: FloconMessageSender, -) : FloconPlugin, FloconDeeplinksPlugin { - - private val deeplinks = MutableStateFlow?>(null) - private val variables = MutableStateFlow?>(null) - - override fun onMessageReceived( - method: String, - body: String, - ) { - // no op - } - - override fun onConnectedToServer() { - // on connected, send known deeplinks - deeplinks.value?.let { - registerDeeplinks(it, variables.value.orEmpty()) - } - } - - override fun registerDeeplinks( - deeplinks: List, - variables: List - ) { - this.deeplinks.update { deeplinks } - this.variables.update { variables } - - try { - sender.send( - plugin = Protocol.FromDevice.Deeplink.Plugin, - method = Protocol.FromDevice.Deeplink.Method.GetDeeplinks, - body = toDeeplinksJson( - deeplinks = deeplinks, - variables = variables - ) - ) - } catch (t: Throwable) { - FloconLogger.logError("deeplink mapping error", t) - } - } -} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt deleted file mode 100644 index 4dede8700..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt +++ /dev/null @@ -1,53 +0,0 @@ -package io.github.openflocon.flocon.plugins.deeplinks - -import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkRemote -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkVariableRemote -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinksRemote - -internal fun toDeeplinksJson( - deeplinks: List, - variables: List -): String { - val dto = DeeplinksRemote( - deeplinks = deeplinks.map(DeeplinkModel::toRemote), - variables = variables.map(DeeplinkVariable::toRemote) - ) - - return FloconEncoder.json - .encodeToString( - serializer = DeeplinksRemote.serializer(), - value = dto - ) -} - -internal fun DeeplinkModel.toRemote(): DeeplinkRemote = DeeplinkRemote( - label = label, - link = link, - description = description, - parameters = parameters.map(DeeplinkModel.Parameter::toRemote) -) - -internal fun DeeplinkVariable.toRemote(): DeeplinkVariableRemote = DeeplinkVariableRemote( - name = name, - mode = when (val mode = mode) { - is DeeplinkVariable.Mode.AutoComplete -> DeeplinkVariableRemote.Mode.AutoComplete(suggestions = mode.suggestions) - DeeplinkVariable.Mode.Input -> DeeplinkVariableRemote.Mode.Input - }, - description = description, -) - -internal fun DeeplinkModel.Parameter.toRemote(): DeeplinkParameterRemote = when (this) { - is DeeplinkModel.Parameter.AutoComplete -> DeeplinkParameterRemote.AutoComplete( - name = paramName, - autoComplete = autoComplete - ) - - is DeeplinkModel.Parameter.Variable -> DeeplinkParameterRemote.Variable( - name = paramName, - variableName = variableName - ) -} - diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt deleted file mode 100644 index e6e9015cc..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt +++ /dev/null @@ -1,65 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) - -package io.github.openflocon.flocon.plugins.deeplinks.model - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonClassDiscriminator - -@Serializable -@JsonClassDiscriminator("type") -internal sealed interface DeeplinkParameterRemote { - val name: String - - @Serializable - @SerialName("auto_complete") - data class AutoComplete( - override val name: String, - val autoComplete: List - ) : DeeplinkParameterRemote - - @Serializable - @SerialName("variable") - data class Variable( - override val name: String, - val variableName: String - ) : DeeplinkParameterRemote -} - -@Serializable -internal class DeeplinkRemote( - val label: String? = null, - val link: String, - val description: String? = null, - val parameters: List -) - -@Serializable -internal data class DeeplinkVariableRemote( - val name: String, - val mode: Mode = Mode.Input, - val description: String? = null -) { - - @Serializable - @JsonClassDiscriminator("type") - sealed interface Mode { - - @Serializable - @SerialName("input") - data object Input : Mode - - @Serializable - @SerialName("auto_complete") - data class AutoComplete(val suggestions: List) : Mode - - } - -} - -@Serializable -internal class DeeplinksRemote( - val deeplinks: List, - val variables: List -) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt index f67e4581b..0afce2adb 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt @@ -3,8 +3,10 @@ package io.github.openflocon.flocon.plugins.device import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.plugins.device.model.fromdevice.RegisterDeviceDataModel +import io.github.openflocon.flocon.pluginsold.device.FloconDeviceConfig +import io.github.openflocon.flocon.pluginsold.device.FloconDevicePlugin -actual object FloconDevice : FloconPluginFactory { +object FloconDevice : FloconPluginFactory { override val name: String = "Device" override val pluginId: String = Protocol.ToDevice.Device.Plugin override fun createConfig() = FloconDeviceConfig() @@ -22,6 +24,7 @@ internal class FloconDevicePluginImpl( private var sender: FloconMessageSender, private val context: FloconContext, ) : FloconPlugin, FloconDevicePlugin { + override val key: String = "DEVICE" override fun registerWithSerial(serial: String) { try { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt index 7761af4b0..b00478108 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt @@ -1,6 +1,12 @@ package io.github.openflocon.flocon.plugins.files -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconFileSender import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.model.FloconFileInfo @@ -11,10 +17,12 @@ import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceDeleteFi import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceDeleteFolderContentMessage import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceGetFileMessage import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceGetFilesMessage +import io.github.openflocon.flocon.pluginsold.files.FloconFilesConfig +import io.github.openflocon.flocon.pluginsold.files.FloconFilesPlugin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -actual object FloconFiles : FloconPluginFactory { +object FloconFiles : FloconPluginFactory { override val name: String = "Files" override val pluginId: String = Protocol.ToDevice.Files.Plugin override fun createConfig() = FloconFilesConfig() @@ -30,13 +38,18 @@ actual object FloconFiles : FloconPluginFactory + fun getFolderContent( + path: String, + isConstantPath: Boolean, + withFoldersSize: Boolean + ): List + fun deleteFile(path: String) fun deleteFiles(path: List) fun deleteFolderContent(folder: FloconFile) } -internal expect fun fileDataSource(context: FloconContext) : FileDataSource +internal expect fun fileDataSource(context: FloconContext): FileDataSource internal class FloconFilesPluginImpl( private val context: FloconContext, @@ -44,6 +57,7 @@ internal class FloconFilesPluginImpl( private val sender: FloconMessageSender, ) : FloconPlugin, FloconFilesPlugin { + override val key: String = "FILES" private val fileDataSource = fileDataSource(context) private val withFoldersSize = MutableStateFlow(false) @@ -67,15 +81,16 @@ internal class FloconFilesPluginImpl( Protocol.ToDevice.Files.Method.GetFile -> { val getFileMessage = ToDeviceGetFileMessage.fromJson(message = body) ?: return - fileDataSource.getFile(path = getFileMessage.path, isConstantPath = false)?.let { file -> - floconFileSender.send( - file = file, - infos = FloconFileInfo( - requestId = getFileMessage.requestId, - path = getFileMessage.path, + fileDataSource.getFile(path = getFileMessage.path, isConstantPath = false) + ?.let { file -> + floconFileSender.send( + file = file, + infos = FloconFileInfo( + requestId = getFileMessage.requestId, + path = getFileMessage.path, + ) ) - ) - } + } } Protocol.ToDevice.Files.Method.DeleteFile -> { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt index 51f23c8cc..b6d4d9048 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt @@ -1,9 +1,27 @@ package io.github.openflocon.flocon.plugins.network -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.network.mapper.* -import io.github.openflocon.flocon.plugins.network.model.* +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.floconNetworkWebSocketEventToJson +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.parseWebSocketMockMessage +import io.github.openflocon.flocon.plugins.network.mapper.webSocketIdsToJsonArray +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkConfig +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketMockListener +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -13,7 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -actual object FloconNetwork : FloconPluginFactory { +object FloconNetwork : FloconPluginFactory { override val name: String = "Network" override val pluginId: String = Protocol.ToDevice.Network.Plugin override fun createConfig() = FloconNetworkConfig() @@ -31,7 +49,7 @@ internal const val FLOCON_NETWORK_BAD_CONFIG_JSON = "flocon_network_bad_config.j internal interface FloconNetworkDataSource { fun saveMocksToFile(mocks: List) - fun loadMocksFromFile() : List + fun loadMocksFromFile(): List fun saveBadNetworkConfig(config: BadQualityConfig?) fun loadBadNetworkConfig(): BadQualityConfig? } @@ -43,16 +61,19 @@ internal class FloconNetworkPluginImpl( private var sender: FloconMessageSender, private val coroutineScope: CoroutineScope, ) : FloconPlugin, FloconNetworkPlugin { + override val key: String = "NETWORK" private val dataSource = buildFloconNetworkDataSource(context) - private val websocketListeners = MutableStateFlow>(emptyMap()) + private val websocketListeners = + MutableStateFlow>(emptyMap()) private val _mocks = MutableStateFlow>(dataSource.loadMocksFromFile()) - override val mocks : List + override val mocks: List get() = _mocks.value - private val _badQualityConfig = MutableStateFlow(dataSource.loadBadNetworkConfig()) + private val _badQualityConfig = + MutableStateFlow(dataSource.loadBadNetworkConfig()) override val badQualityConfig: BadQualityConfig? get() = _badQualityConfig.value @@ -119,7 +140,7 @@ internal class FloconNetworkPluginImpl( Protocol.ToDevice.Network.Method.WebsocketMockMessage -> { val message = parseWebSocketMockMessage(jsonString = body) - if(message != null) { + if (message != null) { websocketListeners.value[message.id]?.onMessage(message.message) } } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt index fbda0f50f..97112eb6f 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt @@ -2,10 +2,9 @@ package io.github.openflocon.flocon.plugins.network.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json internal fun BadQualityConfig.toJsonString(): String { return FloconEncoder.json.encodeToString( @@ -24,6 +23,7 @@ internal fun parseBadQualityConfig(jsonString: String): BadQualityConfig? { null } } + @Serializable internal class BadQualityConfigSerializable( val latency: LatencySerializable, @@ -63,6 +63,7 @@ internal fun BadQualityConfig.toSerializable(): BadQualityConfigSerializable { errorBody = t.errorBody, errorContentType = t.errorContentType ) + is BadQualityConfig.Error.Type.ErrorThrow -> BadQualityConfigSerializable.ErrorSerializable( weight = error.weight, errorException = t.classPath diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt index d187e7bb2..e8b849d6c 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt @@ -3,9 +3,9 @@ package io.github.openflocon.flocon.plugins.network.mapper import io.github.openflocon.flocon.core.FloconEncoder -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.FloconWebSocketEvent +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlin.uuid.ExperimentalUuidApi @@ -14,8 +14,8 @@ import kotlin.uuid.Uuid @Serializable internal class FloconNetworkCallRequestRemote( val floconCallId: String, - val floconNetworkType : String, - val isMocked : Boolean, + val floconNetworkType: String, + val isMocked: Boolean, val url: String, val method: String, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt index 94d681dd8..fd6efd088 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt @@ -2,7 +2,7 @@ package io.github.openflocon.flocon.plugins.network.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString @@ -31,7 +31,8 @@ internal class MockNetworkResponseDataModel( internal fun parseMockResponses(jsonString: String): List { try { - val remote = FloconEncoder.json.decodeFromString>(jsonString) + val remote = + FloconEncoder.json.decodeFromString>(jsonString) return remote.mapNotNull { it.toDomain() } @@ -85,7 +86,7 @@ internal fun writeMockResponsesToJson(mocks: List): String } } -private fun MockNetworkResponse.toRemote(): MockNetworkResponseDataModel? { +private fun MockNetworkResponse.toRemote(): MockNetworkResponseDataModel { return MockNetworkResponseDataModel( expectation = MockNetworkResponseDataModel.Expectation( urlPattern = expectation.urlPattern, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt index 7b5f526f4..1e201a2ba 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -2,11 +2,11 @@ package io.github.openflocon.flocon.plugins.sharedprefs import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.sharedprefs.mapper.toJson -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconSharedPreferenceModel -import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.SetSharedPreferenceValueMessage +import io.github.openflocon.flocon.pluginsold.sharedprefs.FloconPreferencesConfig +import io.github.openflocon.flocon.pluginsold.sharedprefs.FloconPreferencesPlugin +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconSharedPreferenceModel -actual object FloconPreferences : FloconPluginFactory { +object FloconPreferences : FloconPluginFactory { override val name: String = "Preferences" override val pluginId: String = Protocol.ToDevice.SharedPreferences.Plugin override fun createConfig() = FloconPreferencesConfig() @@ -30,6 +30,7 @@ internal class FloconSharedPrefsPluginImpl( private val context: FloconContext, private val sender: FloconMessageSender, ) : FloconPlugin, FloconPreferencesPlugin { + override val key: String = "SHARED_PREF" private val dataSource = buildFloconSharedPreferenceDataSource(context) private val preferenceModels = mutableListOf() @@ -38,27 +39,27 @@ internal class FloconSharedPrefsPluginImpl( method: String, body: String, ) { - when (method) { - Protocol.ToDevice.SharedPreferences.Method.GetSharedPreferences -> { - sendSharedPreferences() - } - - Protocol.ToDevice.SharedPreferences.Method.GetSharedPreferenceValue -> { - // Not implemented yet on device side, usually handled by getSharedPreferences - } - - Protocol.ToDevice.SharedPreferences.Method.SetSharedPreferenceValue -> { - SetSharedPreferenceValueMessage.fromJson(body)?.let { message -> - dataSource.setSharedPreferenceValue( - fileName = message.fileName, - key = message.key, - value = message.value - ) - // Refresh view - sendSharedPreferences() - } - } - } +// when (method) { +// Protocol.ToDevice.SharedPreferences.Method.GetSharedPreferences -> { +// sendSharedPreferences() +// } +// +// Protocol.ToDevice.SharedPreferences.Method.GetSharedPreferenceValue -> { +// // Not implemented yet on device side, usually handled by getSharedPreferences +// } +// +// Protocol.ToDevice.SharedPreferences.Method.SetSharedPreferenceValue -> { +// SetSharedPreferenceValueMessage.fromJson(body)?.let { message -> +// dataSource.setSharedPreferenceValue( +// fileName = message.fileName, +// key = message.key, +// value = message.value +// ) +// // Refresh view +// sendSharedPreferences() +// } +// } +// } } override fun onConnectedToServer() { @@ -73,11 +74,11 @@ internal class FloconSharedPrefsPluginImpl( private fun sendSharedPreferences() { val allPrefs = dataSource.getSharedPreferences() + preferenceModels try { - sender.send( - plugin = Protocol.FromDevice.SharedPreferences.Plugin, - method = Protocol.FromDevice.SharedPreferences.Method.GetSharedPreferences, - body = allPrefs.toJson().toString() - ) +// sender.send( +// plugin = Protocol.FromDevice.SharedPreferences.Plugin, +// method = Protocol.FromDevice.SharedPreferences.Method.GetSharedPreferences, +// body = allPrefs.toJson().toString() +// ) } catch (t: Throwable) { FloconLogger.logError("SharedPreferences json mapping error", t) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt index bcf74a15c..d04e3e16a 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.plugins.sharedprefs.model import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt index c52587f62..9524a8a0b 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt @@ -2,10 +2,12 @@ package io.github.openflocon.flocon.plugins.tables import io.github.openflocon.flocon.* import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.tables.model.TableItem import io.github.openflocon.flocon.plugins.tables.model.tableItemListToJson +import io.github.openflocon.flocon.pluginsold.tables.FloconTableConfig +import io.github.openflocon.flocon.pluginsold.tables.FloconTablePlugin +import io.github.openflocon.flocon.pluginsold.tables.model.TableItem -actual object FloconTable : FloconPluginFactory { + object FloconTable : FloconPluginFactory { override val name: String = "Table" override val pluginId: String = Protocol.ToDevice.Table.Plugin override fun createConfig() = FloconTableConfig() @@ -19,6 +21,7 @@ actual object FloconTable : FloconPluginFactory { + override fun createConfig(): FloconAnalyticsConfig = TODO() + override fun install( + config: FloconAnalyticsConfig, + app: FloconApp + ): FloconAnalyticsPlugin = TODO() + + override val name: String = "" +} +// +//fun floconAnalytics(analyticsName: String) : AnalyticsBuilder { +// return AnalyticsBuilder( +// analyticsTableId = analyticsName, +// analyticsPlugin = FloconApp.instance?.client?.analyticsPlugin, +// ) +//} + +//fun FloconApp.analytics(analyticsName: String): AnalyticsBuilder { +// return AnalyticsBuilder( +// analyticsTableId = analyticsName, +// analyticsPlugin = this.client?.analyticsPlugin, +// ) +//} + +interface FloconAnalyticsPlugin : FloconPlugin { + fun registerAnalytics(analyticsItems: List) +} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt similarity index 73% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt index 6b78860c3..f59c51620 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt @@ -1,10 +1,10 @@ @file:OptIn(ExperimentalUuidApi::class) -package io.github.openflocon.flocon.plugins.analytics.builder +package io.github.openflocon.flocon.pluginsold.analytics.builder -import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsPlugin +import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsEvent +import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem import io.github.openflocon.flocon.utils.currentTimeMillis import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt similarity index 80% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt index 051c7c277..10a608249 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.analytics.model +package io.github.openflocon.flocon.pluginsold.analytics.model data class AnalyticsEvent( val eventName: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt similarity index 76% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt index 4d1af07f7..de6d93092 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.analytics.model +package io.github.openflocon.flocon.pluginsold.analytics.model data class AnalyticsPropertiesConfig( val name: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/TableItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt similarity index 74% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/TableItem.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt index f11507629..c23f13409 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/TableItem.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.analytics.model +package io.github.openflocon.flocon.pluginsold.analytics.model data class AnalyticsItem( val id: String, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt new file mode 100644 index 000000000..a4de6eaf9 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt @@ -0,0 +1,16 @@ +package io.github.openflocon.flocon.pluginsold.crashreporter + +import io.github.openflocon.flocon.FloconPlugin + +class FloconCrashReporterConfig { + var catchFatalErrors: Boolean = true +} + +/** + * Flocon Crash Reporter Plugin. + */ +//expect object FloconCrashReporter : FloconPluginFactory +// +interface FloconCrashReporterPlugin : FloconPlugin { + fun setupCrashHandler() +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/FloconDashboardPlugin.kt new file mode 100644 index 000000000..f0dde5d6a --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/FloconDashboardPlugin.kt @@ -0,0 +1,15 @@ +package io.github.openflocon.flocon.pluginsold.dashboard + +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig + +class FloconDashboardConfig + +/** + * Flocon Dashboard Plugin. + */ +//expect object FloconDashboard : FloconPluginFactory +// +interface FloconDashboardPlugin : FloconPlugin { + fun registerDashboard(dashboardConfig: DashboardConfig) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/ContainerBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/ContainerBuilder.kt new file mode 100644 index 000000000..43a113fc3 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/ContainerBuilder.kt @@ -0,0 +1,14 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.builder + +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ContainerConfig +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ElementConfig + +abstract class ContainerBuilder { + open val elements = mutableListOf() + + open fun add(element: ElementConfig) { + elements.add(element) + } + + abstract fun build(): ContainerConfig +} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/DashboardBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/DashboardBuilder.kt similarity index 53% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/DashboardBuilder.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/DashboardBuilder.kt index 6672693f1..879c6fcc7 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/DashboardBuilder.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/DashboardBuilder.kt @@ -1,8 +1,8 @@ -package io.github.openflocon.flocon.plugins.dashboard.builder +package io.github.openflocon.flocon.pluginsold.dashboard.builder -import io.github.openflocon.flocon.plugins.dashboard.dsl.DashboardDsl -import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig -import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.DashboardDsl +import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ContainerConfig @DashboardDsl class DashboardBuilder(private val id: String) { diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/FormBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/FormBuilder.kt similarity index 73% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/FormBuilder.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/FormBuilder.kt index 8dd5b8b1f..1e43250d9 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/FormBuilder.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/FormBuilder.kt @@ -1,6 +1,6 @@ -package io.github.openflocon.flocon.plugins.dashboard.builder +package io.github.openflocon.flocon.pluginsold.dashboard.builder -import io.github.openflocon.flocon.plugins.dashboard.model.config.FormConfig +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.FormConfig class FormBuilder( val name: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/SectionBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/SectionBuilder.kt similarity index 51% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/SectionBuilder.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/SectionBuilder.kt index 952aa4261..bf301ca26 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/SectionBuilder.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/builder/SectionBuilder.kt @@ -1,6 +1,6 @@ -package io.github.openflocon.flocon.plugins.dashboard.builder +package io.github.openflocon.flocon.pluginsold.dashboard.builder -import io.github.openflocon.flocon.plugins.dashboard.model.config.SectionConfig +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.SectionConfig class SectionBuilder(val name: String) : ContainerBuilder() { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/ButtonDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/ButtonDsl.kt new file mode 100644 index 000000000..7ae6e6479 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/ButtonDsl.kt @@ -0,0 +1,19 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.dsl + +import io.github.openflocon.flocon.pluginsold.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ButtonConfig + +@DashboardDsl +fun ContainerBuilder.button( + text: String, + id: String, + onClick: () -> Unit, +) { + add( + ButtonConfig( + text = text, + id = id, + onClick = onClick, + ) + ) +} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/CheckBoxDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/CheckBoxDsl.kt similarity index 57% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/CheckBoxDsl.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/CheckBoxDsl.kt index 6569df699..43102e7a0 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/CheckBoxDsl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/CheckBoxDsl.kt @@ -1,7 +1,7 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl +package io.github.openflocon.flocon.pluginsold.dashboard.dsl -import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.config.CheckBoxConfig +import io.github.openflocon.flocon.pluginsold.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.CheckBoxConfig @DashboardDsl fun ContainerBuilder.checkBox( diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/DashboardDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/DashboardDsl.kt similarity index 53% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/DashboardDsl.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/DashboardDsl.kt index c59db318b..39e96f5cd 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/DashboardDsl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/DashboardDsl.kt @@ -1,7 +1,7 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl +package io.github.openflocon.flocon.pluginsold.dashboard.dsl -import io.github.openflocon.flocon.plugins.dashboard.builder.DashboardBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig +import io.github.openflocon.flocon.pluginsold.dashboard.builder.DashboardBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig @DslMarker annotation class DashboardDsl diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/FormDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/FormDsl.kt similarity index 61% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/FormDsl.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/FormDsl.kt index f73e6b817..99ff45560 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/FormDsl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/FormDsl.kt @@ -1,7 +1,7 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl +package io.github.openflocon.flocon.pluginsold.dashboard.dsl -import io.github.openflocon.flocon.plugins.dashboard.builder.DashboardBuilder -import io.github.openflocon.flocon.plugins.dashboard.builder.FormBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.builder.DashboardBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.builder.FormBuilder @DashboardDsl fun DashboardBuilder.form( diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/HtmlDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/HtmlDsl.kt new file mode 100644 index 000000000..050d9f612 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/HtmlDsl.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.dsl + +import io.github.openflocon.flocon.pluginsold.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.HtmlConfig + +@DashboardDsl +fun ContainerBuilder.html(label: String, value: String) { + add(HtmlConfig(label = label, value = value)) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/MarkdownDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/MarkdownDsl.kt new file mode 100644 index 000000000..cebfbbf99 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/MarkdownDsl.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.dsl + +import io.github.openflocon.flocon.pluginsold.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.MarkdownConfig + +@DashboardDsl +fun ContainerBuilder.markdown(label: String, value: String) { + add(MarkdownConfig(label = label, value = value)) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/PlainTextDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/PlainTextDsl.kt new file mode 100644 index 000000000..9cff79157 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/PlainTextDsl.kt @@ -0,0 +1,26 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.dsl + +import io.github.openflocon.flocon.pluginsold.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.PlainTextConfig + +@DashboardDsl +fun ContainerBuilder.plainText(label: String, value: String) { + add( + PlainTextConfig( + label = label, + value = value, + type = "text", + ) + ) +} + +@DashboardDsl +fun ContainerBuilder.json(label: String, value: String) { + add( + PlainTextConfig( + label = label, + value = value, + type = "json", + ) + ) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/SectionDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/SectionDsl.kt new file mode 100644 index 000000000..6560ba3a0 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/SectionDsl.kt @@ -0,0 +1,13 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.dsl + +import io.github.openflocon.flocon.pluginsold.dashboard.builder.DashboardBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.builder.SectionBuilder + +@DashboardDsl +fun DashboardBuilder.section(name: String, block: SectionBuilder.() -> Unit) { + val builder = SectionBuilder(name).apply { + block() + } + + add(builder.build()) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/TextDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/TextDsl.kt new file mode 100644 index 000000000..2b13da09a --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/TextDsl.kt @@ -0,0 +1,15 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.dsl + +import io.github.openflocon.flocon.pluginsold.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.LabelConfig +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.TextConfig + +@DashboardDsl +fun ContainerBuilder.text(label: String, value: String, color: Int? = null) { + add(TextConfig(label = label, value = value, color = color)) +} + +@DashboardDsl +fun ContainerBuilder.label(label: String, color: Int? = null) { + add(LabelConfig(label = label, color = color)) +} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextField.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/TextField.kt similarity index 62% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextField.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/TextField.kt index 42bf0bc4e..11717ba59 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextField.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/dsl/TextField.kt @@ -1,7 +1,7 @@ -package io.github.openflocon.flocon.plugins.dashboard.dsl +package io.github.openflocon.flocon.pluginsold.dashboard.dsl -import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder -import io.github.openflocon.flocon.plugins.dashboard.model.config.TextFieldConfig +import io.github.openflocon.flocon.pluginsold.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.TextFieldConfig @DashboardDsl fun ContainerBuilder.textField( diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/ContainerType.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/ContainerType.kt new file mode 100644 index 000000000..a866d9e5a --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/ContainerType.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.model + +enum class ContainerType { + FORM, + SECTION +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/Dashboard.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/Dashboard.kt new file mode 100644 index 000000000..8d8a424f3 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/Dashboard.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.model + +import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ContainerConfig + +data class DashboardConfig( + val id: String, + val containers: List +) \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardScope.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/DashboardScope.kt similarity index 72% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardScope.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/DashboardScope.kt index 3089b07f8..725c56a07 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardScope.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/DashboardScope.kt @@ -1,13 +1,13 @@ -package io.github.openflocon.flocon.plugins.dashboard.model +package io.github.openflocon.flocon.pluginsold.dashboard.model -import io.github.openflocon.flocon.plugins.dashboard.builder.FormBuilder -import io.github.openflocon.flocon.plugins.dashboard.builder.SectionBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.builder.FormBuilder +import io.github.openflocon.flocon.pluginsold.dashboard.builder.SectionBuilder import kotlinx.coroutines.flow.Flow interface DashboardScope { fun section(name: String, flow: Flow, content: SectionBuilder.(T) -> Unit) fun section(name: String, content: SectionBuilder.() -> Unit) - + fun form( name: String, submitText: String = "Submit", @@ -15,7 +15,7 @@ interface DashboardScope { flow: Flow, content: FormBuilder.(T) -> Unit ) - + fun form( name: String, submitText: String = "Submit", diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ButtonConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ButtonConfig.kt similarity index 61% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ButtonConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ButtonConfig.kt index 6fa9d1acb..3e54a5568 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ButtonConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ButtonConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config data class ButtonConfig( val text: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/CheckBoxConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/CheckBoxConfig.kt similarity index 68% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/CheckBoxConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/CheckBoxConfig.kt index f963111a4..de6453454 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/CheckBoxConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/CheckBoxConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config data class CheckBoxConfig( val id: String, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ContainerConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ContainerConfig.kt new file mode 100644 index 000000000..53bf15766 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ContainerConfig.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.model.config + +import io.github.openflocon.flocon.pluginsold.dashboard.model.ContainerType + +sealed interface ContainerConfig { + val name: String + val elements: List + val containerType: ContainerType +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ElementConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ElementConfig.kt new file mode 100644 index 000000000..71a56097a --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/ElementConfig.kt @@ -0,0 +1,3 @@ +package io.github.openflocon.flocon.pluginsold.dashboard.model.config + +sealed interface ElementConfig \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/FormConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/FormConfig.kt similarity index 66% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/FormConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/FormConfig.kt index 51ecb3685..644c96da7 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/FormConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/FormConfig.kt @@ -1,6 +1,6 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config -import io.github.openflocon.flocon.plugins.dashboard.model.ContainerType +import io.github.openflocon.flocon.pluginsold.dashboard.model.ContainerType data class FormConfig( override val name: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/HtmlConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/HtmlConfig.kt similarity index 55% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/HtmlConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/HtmlConfig.kt index 70fce9994..f3de7f22a 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/HtmlConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/HtmlConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config data class HtmlConfig( val label: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/LabelConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/LabelConfig.kt similarity index 55% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/LabelConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/LabelConfig.kt index 24416e4cd..3e9f9a1f7 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/LabelConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/LabelConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config data class LabelConfig( val label: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/MarkdownConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/MarkdownConfig.kt similarity index 56% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/MarkdownConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/MarkdownConfig.kt index f2e4a3799..7b64738d5 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/MarkdownConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/MarkdownConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config data class MarkdownConfig( val label: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/PlainTextConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/PlainTextConfig.kt similarity index 65% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/PlainTextConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/PlainTextConfig.kt index e42961a6e..53c37df21 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/PlainTextConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/PlainTextConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config data class PlainTextConfig( val label: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/SectionConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/SectionConfig.kt similarity index 57% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/SectionConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/SectionConfig.kt index 98b4fe3f9..b2654befa 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/SectionConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/SectionConfig.kt @@ -1,6 +1,6 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config -import io.github.openflocon.flocon.plugins.dashboard.model.ContainerType +import io.github.openflocon.flocon.pluginsold.dashboard.model.ContainerType data class SectionConfig( override val name: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/TextConfig.kt similarity index 61% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/TextConfig.kt index 97ee703a2..5a54816b5 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/TextConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config data class TextConfig( val label: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextFieldConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/TextFieldConfig.kt similarity index 72% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextFieldConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/TextFieldConfig.kt index 9e47cab88..bf2e5ed4a 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextFieldConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/model/config/TextFieldConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.dashboard.model.config +package io.github.openflocon.flocon.pluginsold.dashboard.model.config data class TextFieldConfig( val id: String, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt new file mode 100644 index 000000000..36a04ae60 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt @@ -0,0 +1,34 @@ +package io.github.openflocon.flocon.pluginsold.database + +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.pluginsold.database.model.FloconDatabaseModel + +class FloconDatabaseConfig + +/** + * Flocon Database Plugin. + * Used to inspect Room or other SQL databases. + */ +object FloconDatabase : FloconPluginFactory { + override fun createConfig(): FloconDatabaseConfig { + TODO("Not yet implemented") + } + + override fun install( + config: FloconDatabaseConfig, + app: FloconApp + ): FloconDatabasePlugin { + TODO("Not yet implemented") + } + + override val name: String + get() = TODO("Not yet implemented") +} + + +interface FloconDatabasePlugin : FloconPlugin { + fun register(floconDatabaseModel: FloconDatabaseModel) + fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) +} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt similarity index 75% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt index b75ab65f6..4298eb2eb 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.database.model +package io.github.openflocon.flocon.pluginsold.database.model interface FloconDatabaseModel { val displayName: String diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt new file mode 100644 index 000000000..e35f0bc85 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt @@ -0,0 +1,28 @@ +package io.github.openflocon.flocon.pluginsold.device + +import io.github.openflocon.flocon.* + +class FloconDeviceConfig + +/** + * Flocon Device Plugin. + */ +object FloconDevice : FloconPluginFactory { + override fun createConfig(): FloconDeviceConfig { + TODO("Not yet implemented") + } + + override fun install( + config: FloconDeviceConfig, + app: FloconApp + ): FloconDevicePlugin { + TODO("Not yet implemented") + } + + override val name: String + get() = TODO("Not yet implemented") +} + +interface FloconDevicePlugin : FloconPlugin { + fun registerWithSerial(serial: String) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt new file mode 100644 index 000000000..00cf0f9b8 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt @@ -0,0 +1,29 @@ +package io.github.openflocon.flocon.pluginsold.files + +import io.github.openflocon.flocon.* + +class FloconFilesConfig { + val roots = mutableListOf() +} + +/** + * Flocon Files Plugin. + * Used to inspect and download files from the device. + */ +object FloconFiles : FloconPluginFactory { + override fun createConfig(): FloconFilesConfig { + TODO("Not yet implemented") + } + + override fun install( + config: FloconFilesConfig, + app: FloconApp + ): FloconFilesPlugin { + TODO("Not yet implemented") + } + + override val name: String + get() = TODO("Not yet implemented") +} + +interface FloconFilesPlugin : FloconPlugin \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt new file mode 100644 index 000000000..f498f2255 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt @@ -0,0 +1,45 @@ +package io.github.openflocon.flocon.pluginsold.network + +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketMockListener +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse + +class FloconNetworkConfig { + var badQualityConfig: BadQualityConfig? = null + val mocks = mutableListOf() +} + +/** + * Flocon Network Plugin. + * Used to inspect HTTP/S and WebSocket calls. + */ +object FloconNetwork : FloconPluginFactory { + override fun createConfig(): FloconNetworkConfig = TODO() + override fun install( + config: FloconNetworkConfig, + app: FloconApp + ): FloconNetworkPlugin = TODO() + + override val name: String = "" +} + + +interface FloconNetworkPlugin : FloconPlugin { + val mocks: Collection + val badQualityConfig: BadQualityConfig? + + fun logRequest(request: FloconNetworkCallRequest) + fun logResponse(response: FloconNetworkCallResponse) + + fun logWebSocket( + event: FloconWebSocketEvent, + ) + + fun registerWebSocketMockListener(id: String, listener: FloconWebSocketMockListener) +} diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/BadQualityConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/BadQualityConfig.kt similarity index 97% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/BadQualityConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/BadQualityConfig.kt index fdac3de51..7fe952d6f 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/BadQualityConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/BadQualityConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.model +package io.github.openflocon.flocon.pluginsold.network.model import io.github.openflocon.flocon.FloconLogger import kotlin.random.Random diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconHttpRequest.kt similarity index 91% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconHttpRequest.kt index 913e72351..5d4af5515 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconHttpRequest.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.model +package io.github.openflocon.flocon.pluginsold.network.model data class FloconNetworkRequest( val url: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconNetworkCallRequest.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallRequest.kt similarity index 73% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconNetworkCallRequest.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallRequest.kt index cfcce2eda..8638105a6 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconNetworkCallRequest.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallRequest.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.model +package io.github.openflocon.flocon.pluginsold.network.model data class FloconNetworkCallRequest( val floconCallId: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconNetworkCallResponse.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallResponse.kt similarity index 76% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconNetworkCallResponse.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallResponse.kt index 898287131..6a8fe9e28 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconNetworkCallResponse.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallResponse.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.model +package io.github.openflocon.flocon.pluginsold.network.model data class FloconNetworkCallResponse( val floconCallId: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketEvent.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketEvent.kt similarity index 87% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketEvent.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketEvent.kt index f2be27bc9..29903e336 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketEvent.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketEvent.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.model +package io.github.openflocon.flocon.pluginsold.network.model import io.github.openflocon.flocon.utils.currentTimeMillis diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketMockListener.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketMockListener.kt similarity index 56% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketMockListener.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketMockListener.kt index f6021ee1f..3cd6b177e 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketMockListener.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketMockListener.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.model +package io.github.openflocon.flocon.pluginsold.network.model interface FloconWebSocketMockListener { fun onMessage(message: String) diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/MockNetworkResponse.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/MockNetworkResponse.kt similarity index 94% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/MockNetworkResponse.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/MockNetworkResponse.kt index 535ca4fa9..a1a51226d 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/model/MockNetworkResponse.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/MockNetworkResponse.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.model +package io.github.openflocon.flocon.pluginsold.network.model data class MockNetworkResponse( val expectation: Expectation, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt new file mode 100644 index 000000000..111ff7278 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt @@ -0,0 +1,34 @@ +package io.github.openflocon.flocon.pluginsold.sharedprefs + +import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconSharedPreferenceModel + +class FloconPreferencesConfig + +/** + * Flocon Preferences Plugin. + * Used to inspect SharedPreferences or other key-value stores. + */ +object FloconPreferences : FloconPluginFactory { + override fun createConfig(): FloconPreferencesConfig { + TODO("Not yet implemented") + } + + override fun install( + config: FloconPreferencesConfig, + app: FloconApp + ): FloconPreferencesPlugin { + TODO("Not yet implemented") + } + + override val name: String + get() = TODO("Not yet implemented") +} + +//fun floconRegisterSharedPreference(sharedPreference: FloconSharedPreferenceModel) { +// FloconApp.instance?.client?.preferencesPlugin?.register(sharedPreference) +//} + +interface FloconPreferencesPlugin : FloconPlugin { + fun register(sharedPreference: FloconSharedPreferenceModel) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt new file mode 100644 index 000000000..9df5cd32a --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt @@ -0,0 +1,2 @@ +package io.github.openflocon.flocon.pluginsold.sharedprefs + diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconPreference.kt similarity index 82% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconPreference.kt index 5a3810f9f..5bbe4634f 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconPreference.kt @@ -1,11 +1,11 @@ -package io.github.openflocon.flocon.plugins.sharedprefs.model +package io.github.openflocon.flocon.pluginsold.sharedprefs.model interface FloconPreference { val name: String suspend fun set( columnName: String, - value: FloconPreferenceValue, + value: FloconPreferenceValue ) suspend fun columns(): List diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt new file mode 100644 index 000000000..e5b8f6b71 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt @@ -0,0 +1,5 @@ +package io.github.openflocon.flocon.pluginsold.sharedprefs.model + +// TODO Get model from git +class FloconSharedPreferenceModel { +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt new file mode 100644 index 000000000..d5cdf32d7 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt @@ -0,0 +1,46 @@ +package io.github.openflocon.flocon.pluginsold.tables + +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.pluginsold.tables.model.TableItem + +class FloconTableConfig + +/** + * Flocon Table Plugin. + * Used to display custom data tables. + */ +object FloconTable : FloconPluginFactory { + override fun createConfig(): FloconTableConfig { + TODO("Not yet implemented") + } + + override fun install( + config: FloconTableConfig, + app: FloconApp + ): FloconTablePlugin { + TODO("Not yet implemented") + } + + override val name: String + get() = TODO("Not yet implemented") +} + +//fun floconTable(tableName: String) : TableBuilder { +// return TableBuilder( +// tableId = tableName, +// tablePlugin = FloconApp.instance?.client?.tablePlugin, +// ) +//} +// +//fun FloconApp.table(tableName: String): TableBuilder { +// return TableBuilder( +// tableId = tableName, +// tablePlugin = this.client?.tablePlugin, +// ) +//} + +interface FloconTablePlugin : FloconPlugin { + fun registerItems(tableItems: List) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt new file mode 100644 index 000000000..484c85387 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt @@ -0,0 +1,20 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package io.github.openflocon.flocon.pluginsold.tables.builder + +import kotlin.uuid.ExperimentalUuidApi + +//class TableBuilder( +// val tableName: String, +// private val tablePlugin: FloconTablePlugin?, +//) { +// fun log(vararg columns: TableColumnConfig) { +// val dashboardConfig = TableItem( +// id = Uuid.random().toString(), +// name = tableName, +// columns = columns.toList(), +// createdAt = currentTimeMillis(), +// ) +// tablePlugin?.registerTable(dashboardConfig) +// } +//} \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableColumnConfig.kt similarity index 75% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableColumnConfig.kt index bfb85182d..857f2d8a8 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableColumnConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.tables.model +package io.github.openflocon.flocon.pluginsold.tables.model data class TableColumnConfig( val columnName: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableItem.kt similarity index 68% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableItem.kt index f4635bd90..ba4485e34 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableItem.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.tables.model +package io.github.openflocon.flocon.pluginsold.tables.model data class TableItem( val id: String, diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.kt similarity index 100% rename from FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.kt rename to FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.kt diff --git a/FloconAndroid/flocon-base/src/iosMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt similarity index 97% rename from FloconAndroid/flocon-base/src/iosMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt rename to FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt index 32b5a87cf..c77d01ab2 100644 --- a/FloconAndroid/flocon-base/src/iosMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt @@ -3,18 +3,17 @@ package io.github.openflocon.flocon actual object FloconLogger { actual var enabled = false private const val TAG = "FloconLogger" - + actual fun logError(text: String, throwable: Throwable?) { if(enabled) { println("ERROR $TAG: $text") throwable?.printStackTrace() } } - + actual fun log(text: String) { if(enabled) { println("$TAG: $text") } } -} - +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt index eb0c19068..0290a8e76 100644 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt @@ -1,17 +1,18 @@ package io.github.openflocon.flocon.plugins.sharedprefs import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference -import io.github.openflocon.flocon.plugins.sharedprefs.model.fromdevice.PreferenceRowDataModel -import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.ToDeviceEditSharedPreferenceValueMessage -internal actual fun buildFloconPreferencesDataSource(context: FloconContext): FloconPreferencesDataSource { - return FloconPreferencesDataSourceIOs() -} +//internal actual fun buildFloconPreferencesDataSource(context: FloconContext): FloconPreferencesDataSource { +// return FloconPreferencesDataSourceIOs() +//} // TODO try to bind with ios storage -internal class FloconPreferencesDataSourceIOs : FloconPreferencesDataSource { - override fun detectLocalPreferences(): List { - return emptyList() - } +//internal class FloconPreferencesDataSourceIOs : FloconPreferencesDataSource { +// override fun detectLocalPreferences(): List { +// return emptyList() +// } +//} + +internal actual fun buildFloconSharedPreferenceDataSource(context: FloconContext): FloconSharedPreferenceDataSource { + TODO("Not yet implemented") } \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/iosMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.ios.kt similarity index 100% rename from FloconAndroid/flocon-base/src/iosMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.ios.kt rename to FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.ios.kt diff --git a/FloconAndroid/flocon-base/src/jvmMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt similarity index 97% rename from FloconAndroid/flocon-base/src/jvmMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt rename to FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt index c7577ef7d..f69122a84 100644 --- a/FloconAndroid/flocon-base/src/jvmMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt +++ b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/FloconLogger.kt @@ -3,18 +3,17 @@ package io.github.openflocon.flocon actual object FloconLogger { actual var enabled = false private const val TAG = "FloconLogger" - + actual fun logError(text: String, throwable: Throwable?) { if(enabled) { System.err.println("$TAG: $text") throwable?.printStackTrace() } } - + actual fun log(text: String) { if(enabled) { println("$TAG: $text") } } -} - +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt index 1fcfea362..645d38c33 100644 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt +++ b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt @@ -2,13 +2,18 @@ package io.github.openflocon.flocon.plugins.sharedprefs import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference -internal actual fun buildFloconPreferencesDataSource(context: FloconContext): FloconPreferencesDataSource { - return FloconPreferencesDataSourceJvm() -} +//internal actual fun buildFloconPreferencesDataSource(context: FloconContext): FloconPreferencesDataSource { +// return FloconPreferencesDataSourceJvm() +//} -internal class FloconPreferencesDataSourceJvm : FloconPreferencesDataSource { - override fun detectLocalPreferences(): List { - return emptyList() - } +//internal class FloconPreferencesDataSourceJvm : FloconPreferencesDataSource { +// override fun detectLocalPreferences(): List { +// return emptyList() +// } +//} + +internal actual fun buildFloconSharedPreferenceDataSource(context: FloconContext): FloconSharedPreferenceDataSource { + TODO("Not yet implemented") } \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.jvm.kt similarity index 100% rename from FloconAndroid/flocon-base/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.jvm.kt rename to FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.jvm.kt diff --git a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts index e43bb3211..4e00098c9 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts @@ -40,7 +40,7 @@ kotlin { } dependencies { - implementation(project(":flocon-base")) + implementation(project(":flocon")) implementation(platform(libs.kotlinx.coroutines.bom)) implementation(libs.kotlinx.coroutines.core) diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/BadQuality.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/BadQuality.kt index ae1b9d370..d69d425ae 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/BadQuality.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/BadQuality.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.grpc -import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import java.io.IOException @Throws(IOException::class) diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcBaseInterceptor.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcBaseInterceptor.kt index 4edd65a0e..ee83bd28f 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcBaseInterceptor.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcBaseInterceptor.kt @@ -2,13 +2,12 @@ package io.github.openflocon.flocon.grpc -import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.grpc.model.RequestHolder import io.github.openflocon.flocon.grpc.model.toHeaders -import io.github.openflocon.flocon.plugins.network.FloconNetworkPlugin -import io.github.openflocon.flocon.plugins.network.model.FloconNetworkRequest -import io.github.openflocon.flocon.plugins.network.model.FloconNetworkResponse +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkResponse import io.grpc.CallOptions import io.grpc.Channel import io.grpc.ClientCall @@ -33,7 +32,7 @@ abstract class FloconGrpcBaseInterceptor( callOptions: CallOptions, next: Channel, ): ClientCall { - val networkPlugin = FloconApp.instance?.client?.networkPlugin + val networkPlugin = TODO()//FloconApp.instance?.client?.networkPlugin if (networkPlugin == null) { // do not intercept if no network plugin, just call return next.newCall(method, callOptions) @@ -101,9 +100,9 @@ private class LoggingForwardingClientCall( callId = callId, request = request ) - floconNetworkPlugin.badQualityConfig?.let { - executeBadQuality(it) - } +// floconNetworkPlugin.badQualityConfig?.let { +// executeBadQuality(it) +// } super.sendMessage(message) } } diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt index be3219ae4..f9cc4f8ad 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt @@ -1,22 +1,19 @@ package io.github.openflocon.flocon.grpc -import io.github.openflocon.flocon.FloconApp -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 -import io.github.openflocon.flocon.plugins.network.model.FloconNetworkResponse +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkResponse internal class FloconGrpcPlugin() { fun reportRequest(callId: String, request: FloconNetworkRequest) { - FloconApp.instance?.client?.networkPlugin?.logRequest( - FloconNetworkCallRequest( - floconCallId = callId, - floconNetworkType = "grpc", - request = request, - isMocked = false, - ) - ) +// FloconApp.instance?.client?.networkPlugin?.logRequest( +// FloconNetworkCallRequest( +// floconCallId = callId, +// floconNetworkType = "grpc", +// request = request, +// isMocked = false, +// ) +// ) } fun reportResponse( @@ -25,16 +22,16 @@ internal class FloconGrpcPlugin() { response: FloconNetworkResponse ) { val responseTime = System.currentTimeMillis() - val durationMs = (responseTime - request.startTime).toDouble() + (responseTime - request.startTime).toDouble() - FloconApp.instance?.client?.networkPlugin?.logResponse( - FloconNetworkCallResponse( - floconCallId = callId, - floconNetworkType = "grpc", - response = response, - durationMs = durationMs, - isMocked = false, - ) - ) +// FloconApp.instance?.client?.networkPlugin?.logResponse( +// FloconNetworkCallResponse( +// floconCallId = callId, +// floconNetworkType = "grpc", +// response = response, +// durationMs = durationMs, +// isMocked = false, +// ) +// ) } } diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt index 9518cb4e4..2f9de14b2 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.grpc.model -import io.github.openflocon.flocon.plugins.network.model.FloconNetworkRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest import kotlinx.coroutines.CompletableDeferred internal data class RequestHolder( diff --git a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts index 3f2d23cd9..f645d5790 100644 --- a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon-base")) + implementation(project(":flocon")) implementation(libs.ktor.client.core) } } diff --git a/FloconAndroid/ktor-interceptor/build.gradle.kts b/FloconAndroid/ktor-interceptor/build.gradle.kts index b7d332a7f..d18d78819 100644 --- a/FloconAndroid/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon-base")) + implementation(project(":flocon")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") implementation(libs.ktor.client.core) } diff --git a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt b/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt index 21cc111ae..0ee37502c 100644 --- a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt +++ b/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.ktor -import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import io.ktor.client.HttpClient import io.ktor.client.call.HttpClientCall import io.ktor.client.request.HttpRequestBuilder diff --git a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt b/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt index 5a947fea8..c20f200e0 100644 --- a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt +++ b/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt @@ -2,29 +2,13 @@ package io.github.openflocon.flocon.ktor -import io.github.openflocon.flocon.FloconApp -import io.github.openflocon.flocon.FloconLogger -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 -import io.github.openflocon.flocon.plugins.network.model.FloconNetworkResponse import io.ktor.client.HttpClientConfig import io.ktor.client.plugins.api.createClientPlugin import io.ktor.client.request.HttpRequest -import io.ktor.client.request.HttpSendPipeline -import io.ktor.client.statement.HttpReceivePipeline +import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.bodyAsChannel -import io.ktor.http.contentType import io.ktor.util.AttributeKey -import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.toByteArray -import io.github.openflocon.flocon.utils.currentTimeMillis -import io.github.openflocon.flocon.utils.currentTimeNanos -import io.ktor.client.call.save -import io.ktor.client.request.HttpRequestBuilder import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid data class FloconNetworkIsImageParams( @@ -45,169 +29,169 @@ val FloconKtorPlugin = createClientPlugin("FloconKtorPlugin", ::FloconKtorPlugin val shouldLogCallback = pluginConfig.shouldLog // Intercept requests - client.sendPipeline.intercept(HttpSendPipeline.Monitoring) { - val floconNetworkPlugin = FloconApp.instance?.client?.networkPlugin - val request: HttpRequestBuilder = context - - if (floconNetworkPlugin == null || !shouldLogCallback(request)) { - request.attributes.put(FLOCON_SHOULD_LOG, false) - proceed() - return@intercept - } - - val floconCallId = Uuid.random().toString() - val floconNetworkType = "http" - val requestedAt = currentTimeMillis() - - // Reads the body without consuming it - val requestBodyString = extractAndReplaceRequestBody(request) - val requestSize = requestBodyString?.encodeToByteArray()?.size?.toLong() - val requestHeadersMap = - request.headers.entries().associate { it.key to it.value.joinToString(",") } - - val mockConfig = findMock(request, floconNetworkPlugin) - val isMocked = mockConfig != null - - val floconNetworkRequest = FloconNetworkRequest( - url = request.url.toString(), - method = request.method.value, - startTime = requestedAt, - headers = requestHeadersMap, - body = requestBodyString, - size = requestSize, - isMocked = isMocked - ) - - floconNetworkPlugin.logRequest( - FloconNetworkCallRequest( - floconCallId = floconCallId, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - request = floconNetworkRequest - ) - ) - - val startTime = currentTimeNanos() - request.attributes.put(FLOCON_CALL_ID_KEY, floconCallId) - request.attributes.put(FLOCON_START_TIME_KEY, startTime) - request.attributes.put(FLOCON_IS_MOCKED_KEY, isMocked) - - try { - if (isMocked) { - val fakeCall = executeMock(client = theClient, request = request, mock = mockConfig) - proceedWith(fakeCall) - return@intercept - } - - floconNetworkPlugin.badQualityConfig?.let { badQualityConfig -> - executeBadQuality( - badQualityConfig = badQualityConfig, - client = theClient, - request = request - ) - } ?: run { - proceed() - } - } catch (t: Throwable) { - val endTime = currentTimeNanos() - - val durationMs: Double = (endTime - startTime) / 1e6 - - val floconCallResponse = FloconNetworkResponse( - httpCode = null, - contentType = null, - body = null, - headers = emptyMap(), - size = null, - grpcStatus = null, - error = t.message ?: t::class.simpleName ?: "Unknown", - requestHeaders = requestHeadersMap, - isImage = false, - ) - - floconNetworkPlugin.logResponse( - FloconNetworkCallResponse( - floconCallId = floconCallId, - durationMs = durationMs, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - response = floconCallResponse, - ) - ) - throw t - } - } - - // Intercepts responses - client.receivePipeline.intercept(HttpReceivePipeline.After) { response -> - val floconNetworkPlugin = FloconApp.instance?.client?.networkPlugin ?: return@intercept - - val savedCall = response.call.save() - val savedResponse = savedCall.response - val call = response.call - val request: HttpRequest = call.request - - val floconShouldLog = request.attributes.getOrNull(FLOCON_SHOULD_LOG) ?: true - if(!floconShouldLog) { - proceed() - return@intercept - } - - val floconCallId = request.attributes.getOrNull(FLOCON_CALL_ID_KEY) ?: return@intercept - val startTime = request.attributes.getOrNull(FLOCON_START_TIME_KEY) ?: return@intercept - val isMocked = request.attributes.getOrNull(FLOCON_IS_MOCKED_KEY) ?: false - - val endTime = currentTimeNanos() - val durationMs = (endTime - startTime) / 1e6 - - val originalBodyBytes = response.bodyAsChannel().toByteArray() - val responseSize = originalBodyBytes.size.toLong() - - val responseHeadersMap = - response.headers.entries().associate { it.key to it.value.joinToString(",") } - val contentType = response.contentType()?.toString() - - val requestHeadersMap = - request.headers.entries().associate { it.key to it.value.joinToString(",") } - - val isImage = contentType?.startsWith("image/") == true || isImageCallback?.invoke( - FloconNetworkIsImageParams( - request = request, - response = response, - responseContentType = contentType, - ) - ) == true - - val floconCallResponse = FloconNetworkResponse( - httpCode = response.status.value, - contentType = contentType, - body = if (isImage) null else { - if (responseHeadersMap.isBrotli()) { - decodeNetworkBody(originalBodyBytes, responseHeadersMap) - } else { - originalBodyBytes.decodeToString() - } - }, - headers = responseHeadersMap, - size = responseSize, - grpcStatus = null, - error = null, - requestHeaders = requestHeadersMap, - isImage = isImage, - ) - - floconNetworkPlugin.logResponse( - FloconNetworkCallResponse( - floconCallId = floconCallId, - durationMs = durationMs, - floconNetworkType = "http", - isMocked = isMocked, - response = floconCallResponse - ) - ) - - proceedWith(savedResponse) - } +// client.sendPipeline.intercept(HttpSendPipeline.Monitoring) { +// val floconNetworkPlugin = FloconApp.instance?.client?.networkPlugin +// val request: HttpRequestBuilder = context +// +// if (floconNetworkPlugin == null || !shouldLogCallback(request)) { +// request.attributes.put(FLOCON_SHOULD_LOG, false) +// proceed() +// return@intercept +// } +// +// val floconCallId = Uuid.random().toString() +// val floconNetworkType = "http" +// val requestedAt = currentTimeMillis() +// +// // Reads the body without consuming it +// val requestBodyString = extractAndReplaceRequestBody(request) +// val requestSize = requestBodyString?.encodeToByteArray()?.size?.toLong() +// val requestHeadersMap = +// request.headers.entries().associate { it.key to it.value.joinToString(",") } +// +// val mockConfig = findMock(request, floconNetworkPlugin) +// val isMocked = mockConfig != null +// +// val floconNetworkRequest = FloconNetworkRequest( +// url = request.url.toString(), +// method = request.method.value, +// startTime = requestedAt, +// headers = requestHeadersMap, +// body = requestBodyString, +// size = requestSize, +// isMocked = isMocked +// ) +// +// floconNetworkPlugin.logRequest( +// FloconNetworkCallRequest( +// floconCallId = floconCallId, +// floconNetworkType = floconNetworkType, +// isMocked = isMocked, +// request = floconNetworkRequest +// ) +// ) +// +// val startTime = currentTimeNanos() +// request.attributes.put(FLOCON_CALL_ID_KEY, floconCallId) +// request.attributes.put(FLOCON_START_TIME_KEY, startTime) +// request.attributes.put(FLOCON_IS_MOCKED_KEY, isMocked) +// +// try { +// if (isMocked) { +// val fakeCall = executeMock(client = theClient, request = request, mock = mockConfig) +// proceedWith(fakeCall) +// return@intercept +// } +// +// floconNetworkPlugin.badQualityConfig?.let { badQualityConfig -> +// executeBadQuality( +// badQualityConfig = badQualityConfig, +// client = theClient, +// request = request +// ) +// } ?: run { +// proceed() +// } +// } catch (t: Throwable) { +// val endTime = currentTimeNanos() +// +// val durationMs: Double = (endTime - startTime) / 1e6 +// +// val floconCallResponse = FloconNetworkResponse( +// httpCode = null, +// contentType = null, +// body = null, +// headers = emptyMap(), +// size = null, +// grpcStatus = null, +// error = t.message ?: t::class.simpleName ?: "Unknown", +// requestHeaders = requestHeadersMap, +// isImage = false, +// ) +// +// floconNetworkPlugin.logResponse( +// FloconNetworkCallResponse( +// floconCallId = floconCallId, +// durationMs = durationMs, +// floconNetworkType = floconNetworkType, +// isMocked = isMocked, +// response = floconCallResponse, +// ) +// ) +// throw t +// } +// } +// +// // Intercepts responses +// client.receivePipeline.intercept(HttpReceivePipeline.After) { response -> +// val floconNetworkPlugin = FloconApp.instance?.client?.networkPlugin ?: return@intercept +// +// val savedCall = response.call.save() +// val savedResponse = savedCall.response +// val call = response.call +// val request: HttpRequest = call.request +// +// val floconShouldLog = request.attributes.getOrNull(FLOCON_SHOULD_LOG) ?: true +// if(!floconShouldLog) { +// proceed() +// return@intercept +// } +// +// val floconCallId = request.attributes.getOrNull(FLOCON_CALL_ID_KEY) ?: return@intercept +// val startTime = request.attributes.getOrNull(FLOCON_START_TIME_KEY) ?: return@intercept +// val isMocked = request.attributes.getOrNull(FLOCON_IS_MOCKED_KEY) ?: false +// +// val endTime = currentTimeNanos() +// val durationMs = (endTime - startTime) / 1e6 +// +// val originalBodyBytes = response.bodyAsChannel().toByteArray() +// val responseSize = originalBodyBytes.size.toLong() +// +// val responseHeadersMap = +// response.headers.entries().associate { it.key to it.value.joinToString(",") } +// val contentType = response.contentType()?.toString() +// +// val requestHeadersMap = +// request.headers.entries().associate { it.key to it.value.joinToString(",") } +// +// val isImage = contentType?.startsWith("image/") == true || isImageCallback?.invoke( +// FloconNetworkIsImageParams( +// request = request, +// response = response, +// responseContentType = contentType, +// ) +// ) == true +// +// val floconCallResponse = FloconNetworkResponse( +// httpCode = response.status.value, +// contentType = contentType, +// body = if (isImage) null else { +// if (responseHeadersMap.isBrotli()) { +// decodeNetworkBody(originalBodyBytes, responseHeadersMap) +// } else { +// originalBodyBytes.decodeToString() +// } +// }, +// headers = responseHeadersMap, +// size = responseSize, +// grpcStatus = null, +// error = null, +// requestHeaders = requestHeadersMap, +// isImage = isImage, +// ) +// +// floconNetworkPlugin.logResponse( +// FloconNetworkCallResponse( +// floconCallId = floconCallId, +// durationMs = durationMs, +// floconNetworkType = "http", +// isMocked = isMocked, +// response = floconCallResponse +// ) +// ) +// +// proceedWith(savedResponse) +// } } fun HttpClientConfig<*>.floconInterceptor() { diff --git a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt b/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt index 03c06c009..57f0da6a8 100644 --- a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt +++ b/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt @@ -1,7 +1,7 @@ package io.github.openflocon.flocon.ktor -import io.github.openflocon.flocon.plugins.network.FloconNetworkPlugin -import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse import io.ktor.client.HttpClient import io.ktor.client.call.HttpClientCall import io.ktor.client.request.HttpRequestBuilder @@ -13,14 +13,12 @@ import io.ktor.util.date.GMTDate import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.InternalAPI import kotlinx.coroutines.delay -import kotlin.collections.component1 -import kotlin.collections.component2 internal fun findMock( request: HttpRequestBuilder, floconNetworkPlugin: FloconNetworkPlugin, ): MockNetworkResponse? { - val url = request.url.toString() + val url = request.url.toString() val method = request.method.value return floconNetworkPlugin.mocks.firstOrNull { it.expectation.matches( @@ -40,7 +38,7 @@ internal suspend fun executeMock( delay(mock.response.delay) } - when(val response = mock.response) { + when (val response = mock.response) { is MockNetworkResponse.Response.Body -> { val bodyBytes = response.body.encodeToByteArray() val headers = HeadersBuilder().apply { @@ -58,6 +56,7 @@ internal suspend fun executeMock( return HttpClientCall(client, request.build(), responseData) } + is MockNetworkResponse.Response.ErrorThrow -> { val error = response.generate() if (error != null) { diff --git a/FloconAndroid/okhttp-interceptor/build.gradle.kts b/FloconAndroid/okhttp-interceptor/build.gradle.kts index 118ccbcbd..5388747f0 100644 --- a/FloconAndroid/okhttp-interceptor/build.gradle.kts +++ b/FloconAndroid/okhttp-interceptor/build.gradle.kts @@ -41,7 +41,7 @@ kotlin { dependencies { - implementation(project(":flocon-base")) + implementation(project(":flocon")) implementation(platform(libs.kotlinx.coroutines.bom)) implementation(libs.jetbrains.kotlinx.coroutines.core) diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt index ec62b25dc..35c443ef4 100644 --- a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt +++ b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.okhttp -import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Protocol import okhttp3.Request @@ -36,7 +36,8 @@ internal fun failResponseIfNeeded( badQualityConfig.selectRandomError()?.let { selectedError -> when (val t = selectedError.type) { is BadQualityConfig.Error.Type.Body -> { - val errorBody = t.errorBody.toResponseBody(t.errorContentType.toMediaTypeOrNull()) + val errorBody = + t.errorBody.toResponseBody(t.errorContentType.toMediaTypeOrNull()) return Response.Builder() .request(request) diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt index 8ded87c24..b09e3f3ac 100644 --- a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt +++ b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt @@ -1,12 +1,12 @@ package io.github.openflocon.flocon.okhttp -import io.github.openflocon.flocon.plugins.network.FloconNetworkPlugin -import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import okhttp3.Response.* +import okhttp3.Response.Builder import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.GzipSink @@ -17,7 +17,7 @@ internal fun findMock( request: Request, floconNetworkPlugin: FloconNetworkPlugin, ): MockNetworkResponse? { - val url = request.url.toString() + val url = request.url.toString() val method = request.method return floconNetworkPlugin.mocks.firstOrNull { it.expectation.matches( @@ -28,7 +28,11 @@ internal fun findMock( } @Throws(IOException::class) -internal fun executeMock(request: Request, requestHeaders: Map, mock: MockNetworkResponse): Response { +internal fun executeMock( + request: Request, + requestHeaders: Map, + mock: MockNetworkResponse +): Response { if (mock.response.delay > 0) { try { Thread.sleep(mock.response.delay) @@ -37,13 +41,14 @@ internal fun executeMock(request: Request, requestHeaders: Map, } } - when(val response = mock.response) { + when (val response = mock.response) { is MockNetworkResponse.Response.Body -> { val mediaType = response.mediaType.toMediaTypeOrNull() var bodyBytes = response.body.toByteArray() // TODO maybe check the mocked response headers - val isGzipped = requestHeaders["Accept-Encoding"] == "gzip" || requestHeaders["accept-encoding"] == "gzip" + val isGzipped = + requestHeaders["Accept-Encoding"] == "gzip" || requestHeaders["accept-encoding"] == "gzip" if (isGzipped) { val buffer = Buffer() diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt index 5312cd0a9..88e0cf095 100644 --- a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt +++ b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt @@ -2,13 +2,8 @@ package io.github.openflocon.flocon.okhttp -import io.github.openflocon.flocon.FloconApp -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 -import io.github.openflocon.flocon.plugins.network.model.FloconNetworkResponse +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest import okhttp3.Interceptor -import okhttp3.MediaType import okhttp3.Request import okhttp3.Response import java.io.IOException @@ -28,14 +23,14 @@ class FloconOkhttpInterceptor( @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { - val floconNetworkPlugin = FloconApp.instance?.client?.networkPlugin + val floconNetworkPlugin = TODO()//FloconApp.instance?.client?.networkPlugin if (floconNetworkPlugin == null || !shouldLog(chain)) { // on no op, do not intercept the call, just execute it return chain.proceed(chain.request()) } - val floconCallId = Uuid.random().toString() - val floconNetworkType = "http" + Uuid.random().toString() + "http" val request = chain.request() @@ -65,7 +60,7 @@ class FloconOkhttpInterceptor( ) val isMocked = mockConfig != null - val floconNetworkRequest = FloconNetworkRequest( + FloconNetworkRequest( url = request.url.toString(), method = request.method, startTime = requestedAt, @@ -75,86 +70,87 @@ class FloconOkhttpInterceptor( isMocked = isMocked, ) - floconNetworkPlugin.logRequest( - FloconNetworkCallRequest( - floconCallId = floconCallId, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - request = floconNetworkRequest, - ) - ) +// floconNetworkPlugin.logRequest( +// FloconNetworkCallRequest( +// floconCallId = floconCallId, +// floconNetworkType = floconNetworkType, +// isMocked = isMocked, +// request = floconNetworkRequest, +// ) +// ) try { - val response = if (isMocked) { - executeMock(request = request, mock = mockConfig, requestHeaders = requestHeadersMap) - } else { - floconNetworkPlugin.badQualityConfig?.let { badQualityConfig -> - executeBadQuality( - badQualityConfig = badQualityConfig, - request = request, - ) - } ?: run { - chain.proceed(request) - } - } + val response = TODO() +// if (isMocked) { +// executeMock(request = request, mock = mockConfig, requestHeaders = requestHeadersMap) +// } else { +// floconNetworkPlugin.badQualityConfig?.let { badQualityConfig -> +// executeBadQuality( +// badQualityConfig = badQualityConfig, +// request = request, +// ) +// } ?: run { +// chain.proceed(request) +// } +// } val endTime = System.nanoTime() - val durationMs: Double = (endTime - startTime) / 1e6 + (endTime - startTime) / 1e6 // To get the response body, be careful // because the body can only be read once. // It must be duplicated so that the chain can continue normally. - val responseBody = response.body - var responseBodyString: String? = null - var responseSize: Long? = null - val responseContentType: MediaType? = responseBody?.contentType() - - val responseHeadersMap = - response.headers.toMultimap().mapValues { it.value.joinToString(",") } - - if (responseBody != null) { - val (bodyString, bodySize) = extractResponseBodyInfo( - response = response, - responseHeaders = responseHeadersMap, - ) - responseBodyString = bodyString - responseSize = bodySize - } - - val isImage = - responseContentType?.toString()?.startsWith("image/") == true || (isImage?.invoke( - FloconNetworkIsImageParams( - request = request, - response = response, - responseContentType = responseContentType?.toString(), - ) - ) == true) - - val requestHeadersMapUpToDate = - response.request.headers.toMultimap().mapValues { it.value.joinToString(",") } - - val floconCallResponse = FloconNetworkResponse( - httpCode = response.code, - contentType = responseContentType?.toString(), - body = responseBodyString.takeUnless { isImage }, // dont send images responses bytes - headers = responseHeadersMap, - size = responseSize, - grpcStatus = null, - error = null, - requestHeaders = requestHeadersMapUpToDate, - isImage = isImage, - ) - - floconNetworkPlugin.logResponse( - FloconNetworkCallResponse( - floconCallId = floconCallId, - durationMs = durationMs, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - response = floconCallResponse, - ) - ) +// val responseBody = response.body +// var responseBodyString: String? = null +// var responseSize: Long? = null +// val responseContentType: MediaType? = responseBody?.contentType() +// +// val responseHeadersMap = +// response.headers.toMultimap().mapValues { it.value.joinToString(",") } +// +// if (responseBody != null) { +// val (bodyString, bodySize) = extractResponseBodyInfo( +// response = response, +// responseHeaders = responseHeadersMap, +// ) +// responseBodyString = bodyString +// responseSize = bodySize +// } + +// val isImage = +// responseContentType?.toString()?.startsWith("image/") == true || (isImage?.invoke( +// FloconNetworkIsImageParams( +// request = request, +// response = response, +// responseContentType = responseContentType?.toString(), +// ) +// ) == true) +// +// val requestHeadersMapUpToDate = +// response.request.headers.toMultimap().mapValues { it.value.joinToString(",") } +// +// val floconCallResponse = FloconNetworkResponse( +// httpCode = response.code, +// contentType = responseContentType?.toString(), +// body = responseBodyString.takeUnless { isImage }, // dont send images responses bytes +// headers = responseHeadersMap, +// size = responseSize, +// grpcStatus = null, +// error = null, +// requestHeaders = requestHeadersMapUpToDate, +// isImage = isImage, +// ) +// +// floconNetworkPlugin.logResponse( +// FloconNetworkCallResponse( +// floconCallId = floconCallId, +// durationMs = durationMs, +// floconNetworkType = floconNetworkType, +// isMocked = isMocked, +// response = floconCallResponse, +// ) +// ) // Rebuild the response with a new body so that the chain can continue // The original response body is already consumed by peekBody, so no need to rebuild with it. @@ -164,29 +160,29 @@ class FloconOkhttpInterceptor( val endTime = System.nanoTime() - val durationMs: Double = (endTime - startTime) / 1e6 - - val floconCallResponse = FloconNetworkResponse( - httpCode = null, - contentType = null, - body = null, - headers = emptyMap(), - size = null, - grpcStatus = null, - error = e.message ?: e.javaClass.simpleName, - requestHeaders = null, - isImage = false, - ) - - floconNetworkPlugin.logResponse( - FloconNetworkCallResponse( - floconCallId = floconCallId, - durationMs = durationMs, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - response = floconCallResponse, - ) - ) + (endTime - startTime) / 1e6 + +// val floconCallResponse = FloconNetworkResponse( +// httpCode = null, +// contentType = null, +// body = null, +// headers = emptyMap(), +// size = null, +// grpcStatus = null, +// error = e.message ?: e.javaClass.simpleName, +// requestHeaders = null, +// isImage = false, +// ) + +// floconNetworkPlugin.logResponse( +// FloconNetworkCallResponse( +// floconCallId = floconCallId, +// durationMs = durationMs, +// floconNetworkType = floconNetworkType, +// isMocked = isMocked, +// response = floconCallResponse, +// ) +// ) throw e } } diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt index 4d02a7d01..d7208acbf 100644 --- a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt +++ b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt @@ -1,9 +1,7 @@ package io.github.openflocon.flocon.okhttp.websocket import io.github.openflocon.flocon.FloconApp -import io.github.openflocon.flocon.plugins.network.floconLogWebSocketEvent -import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketEvent -import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketMockListener +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener @@ -19,15 +17,15 @@ object FloconWebSocket { error: Throwable? = null, ) { val size = message?.toByteArray()?.size?.toLong() - floconLogWebSocketEvent( - FloconWebSocketEvent( - websocketUrl = webSocket.request().url.toString(), - event = event, - message = message, - error = error, - size = size ?: 0L, - ) - ) +// floconLogWebSocketEvent( +// FloconWebSocketEvent( +// websocketUrl = webSocket.request().url.toString(), +// event = event, +// message = message, +// error = error, +// size = size ?: 0L, +// ) +// ) } fun send(webSocket: WebSocket, text: String) : Boolean { @@ -53,13 +51,13 @@ object FloconWebSocket { private var websocketRef : WeakReference? = null init { - FloconApp.instance?.client?.networkPlugin?.registerWebSocketMockListener(id = id, listener = object: FloconWebSocketMockListener { - override fun onMessage(message: String) { - websocketRef?.get()?.let { websocket -> - onMessage(websocket, message) - } - } - }) +// FloconApp.instance?.client?.networkPlugin?.registerWebSocketMockListener(id = id, listener = object: FloconWebSocketMockListener { +// override fun onMessage(message: String) { +// websocketRef?.get()?.let { websocket -> +// onMessage(websocket, message) +// } +// } +// }) } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index 8f0737c2f..ce99fa234 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -20,39 +20,22 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import io.github.openflocon.flocon.Flocon -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.myapplication.dashboard.initializeDashboard +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.myapplication.database.DogDatabase -import io.github.openflocon.flocon.myapplication.database.initializeDatabases import io.github.openflocon.flocon.myapplication.database.initializeInMemoryDatabases import io.github.openflocon.flocon.myapplication.database.model.DogEntity -import io.github.openflocon.flocon.myapplication.deeplinks.initializeDeeplinks -import io.github.openflocon.flocon.myapplication.graphql.GraphQlTester import io.github.openflocon.flocon.myapplication.grpc.GrpcController -import io.github.openflocon.flocon.myapplication.images.initializeImages -import io.github.openflocon.flocon.myapplication.sharedpreferences.initializeDatastores -import io.github.openflocon.flocon.myapplication.sharedpreferences.initializeSharedPreferences -import io.github.openflocon.flocon.myapplication.sharedpreferences.initializeSharedPreferencesAfterInit -import io.github.openflocon.flocon.myapplication.table.initializeTable import io.github.openflocon.flocon.myapplication.ui.ImagesListView import io.github.openflocon.flocon.myapplication.ui.theme.MyApplicationTheme -import io.github.openflocon.flocon.okhttp.FloconOkhttpInterceptor -import io.github.openflocon.flocon.plugins.analytics.analytics -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent -import io.github.openflocon.flocon.plugins.analytics.model.analyticsProperty -import io.github.openflocon.flocon.plugins.tables.model.toParam -import io.github.openflocon.flocon.plugins.tables.table +import io.github.openflocon.flocon.startFlocon import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import okhttp3.OkHttpClient -import kotlin.uuid.Uuid import kotlin.random.Random import kotlin.uuid.ExperimentalUuidApi class MainActivity : ComponentActivity() { - lateinit var inMemoryDb : DogDatabase + lateinit var inMemoryDb: DogDatabase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -62,38 +45,41 @@ class MainActivity : ComponentActivity() { Toast.makeText(this, "opend with : $it", Toast.LENGTH_LONG).show() } - val okHttpClient = OkHttpClient() - .newBuilder() - .addInterceptor(FloconOkhttpInterceptor( - isImage = { - it.request.url.toString().contains("picsum") - }, - /*shouldLog = { - val url = it.request().url.toString() - println("url: $url") - url.contains("1").not() - }*/ - )) - .build() - - initializeSharedPreferences(applicationContext) - initializeDatabases(context = applicationContext) - - FloconLogger.enabled = true - Flocon.initialize(this) - initializeDeeplinks() +// val okHttpClient = OkHttpClient() +// .newBuilder() +// .addInterceptor( +// FloconOkhttpInterceptor( +// isImage = { +// it.request.url.toString().contains("picsum") +// }, +// /*shouldLog = { +// val url = it.request().url.toString() +// println("url: $url") +// url.contains("1").not() +// }*/ +// ) +// ) +// .build() + +// initializeSharedPreferences(applicationContext) +// initializeDatabases(context = applicationContext) + +// FloconLogger.enabled = true +// Flocon.initialize(this) inMemoryDb = initializeInMemoryDatabases(applicationContext) - initializeSharedPreferencesAfterInit(applicationContext) - initializeDatastores(applicationContext) +// initializeSharedPreferencesAfterInit(applicationContext) +// initializeDatastores(applicationContext) - val dummyHttpCaller = DummyHttpCaller(client = okHttpClient) - val dummyWebsocketCaller = DummyWebsocketCaller(client = okHttpClient) - GlobalScope.launch { dummyWebsocketCaller.connectToWebsocket() } - val graphQlTester = GraphQlTester(client = okHttpClient) - initializeImages(context = this, okHttpClient = okHttpClient) - initializeDashboard(this) - initializeTable(this) +// val dummyHttpCaller = DummyHttpCaller(client = okHttpClient) +// val dummyWebsocketCaller = DummyWebsocketCaller(client = okHttpClient) +// GlobalScope.launch { dummyWebsocketCaller.connectToWebsocket() } +// val graphQlTester = GraphQlTester(client = okHttpClient) +// initializeImages(context = this, okHttpClient = okHttpClient) +// initializeDashboard(this) +// initializeTable(this) + + initFlocon() setContent { MyApplicationTheme { @@ -101,7 +87,11 @@ class MainActivity : ComponentActivity() { val scope = rememberCoroutineScope() val context = LocalContext.current - Column(Modifier.fillMaxSize().padding(innerPadding)) { + Column( + Modifier + .fillMaxSize() + .padding(innerPadding) + ) { FlowRow( modifier = Modifier .fillMaxWidth() @@ -111,14 +101,14 @@ class MainActivity : ComponentActivity() { ) { Button( onClick = { - dummyHttpCaller.call() + //dummyHttpCaller.call() } ) { Text("okhttp test") } Button( onClick = { - dummyHttpCaller.callGzip() + //dummyHttpCaller.callGzip() } ) { Text("okhttp gzip test") @@ -126,7 +116,7 @@ class MainActivity : ComponentActivity() { Button( onClick = { GlobalScope.launch { - graphQlTester.fetchViewerInfo() + //graphQlTester.fetchViewerInfo() } } ) { @@ -150,7 +140,7 @@ class MainActivity : ComponentActivity() { } Button( onClick = { - dummyWebsocketCaller.send(Uuid.random().toString()) + //dummyWebsocketCaller.send(Uuid.random().toString()) } ) { Text("websocket test") @@ -164,32 +154,32 @@ class MainActivity : ComponentActivity() { } Button( onClick = { - val value = Random.nextInt(from = 0, until = 1000).toString() - Flocon.table("analytics").log( - "name" toParam "new name $value", - "value1" toParam "value1 $value", - "value2" toParam "value2 $value", - ) + Random.nextInt(from = 0, until = 1000).toString() +// Flocon.table("analytics").log( +// "name" toParam "new name $value", +// "value1" toParam "value1 $value", +// "value2" toParam "value2 $value", +// ) } ) { Text("send table event") } Button( onClick = { - Flocon.analytics("firebase").logEvents( - AnalyticsEvent( - eventName = "clicked user", - "userId" analyticsProperty "1024", - "username" analyticsProperty "florent", - "index" analyticsProperty "3", - ), - AnalyticsEvent( - eventName = "opened profile", - "userId" analyticsProperty "2048", - "username" analyticsProperty "kevin", - "age" analyticsProperty "34", - ), - ) +// Flocon.analytics("firebase").logEvents( +// AnalyticsEvent( +// eventName = "clicked user", +// "userId" analyticsProperty "1024", +// "username" analyticsProperty "florent", +// "index" analyticsProperty "3", +// ), +// AnalyticsEvent( +// eventName = "opened profile", +// "userId" analyticsProperty "2048", +// "username" analyticsProperty "kevin", +// "age" analyticsProperty "34", +// ), +// ) } ) { Text("send analytics event") @@ -219,4 +209,25 @@ class MainActivity : ComponentActivity() { } } } + + private fun initFlocon() { + startFlocon(FloconContext(this)) { +// install(FloconDeeplinks) { +// register("flocon://home") +// register("flocon://test") +// register( +// "flocon://user/[userId]", +// label = "User" +// ) { +// param("userId", listOf("Florent", "David", "Guillaume")) +// } +// register( +// "flocon://post/[postId]?comment=[commentText]", +// label = "Post", +// description = "Open a post and send a comment" +// ) +// } + } + } + } \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt index 02777a5af..bc472fa48 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt @@ -6,20 +6,20 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.lifecycle.lifecycleScope +import com.apollographql.apollo.api.label import io.github.openflocon.flocon.myapplication.dashboard.device.deviceFlow import io.github.openflocon.flocon.myapplication.dashboard.device.initializeDeviceFlow import io.github.openflocon.flocon.myapplication.dashboard.tokens.tokensFlow import io.github.openflocon.flocon.myapplication.dashboard.user.userFlow -import io.github.openflocon.flocon.plugins.dashboard.dsl.button -import io.github.openflocon.flocon.plugins.dashboard.dsl.checkBox -import io.github.openflocon.flocon.plugins.dashboard.dsl.html -import io.github.openflocon.flocon.plugins.dashboard.dsl.json -import io.github.openflocon.flocon.plugins.dashboard.dsl.label -import io.github.openflocon.flocon.plugins.dashboard.dsl.markdown -import io.github.openflocon.flocon.plugins.dashboard.dsl.plainText -import io.github.openflocon.flocon.plugins.dashboard.dsl.text -import io.github.openflocon.flocon.plugins.dashboard.dsl.textField import io.github.openflocon.flocon.plugins.dashboard.floconDashboard +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.button +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.checkBox +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.html +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.json +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.markdown +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.plainText +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.text +import io.github.openflocon.flocon.pluginsold.dashboard.dsl.textField import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt index cce1719d3..c499b51df 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt @@ -8,7 +8,6 @@ import io.github.openflocon.flocon.myapplication.database.dao.DogDao import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.database.model.HumanEntity import io.github.openflocon.flocon.myapplication.database.model.HumanWithDogEntity -import io.github.openflocon.flocon.plugins.database.floconLogDatabaseQuery import java.util.concurrent.Executors @Database( @@ -30,17 +29,18 @@ abstract class DogDatabase : RoomDatabase() { fun getDatabase(context: Context): DogDatabase { val dbName = "dogs_database" return INSTANCE ?: synchronized(this) { - val instance = Room.databaseBuilder( - context.applicationContext, - DogDatabase::class.java, - dbName - ).setQueryCallback({ sqlQuery, bindArgs -> floconLogDatabaseQuery( - dbName = dbName, sqlQuery = sqlQuery, bindArgs = bindArgs - ) }, Executors.newSingleThreadExecutor()) - .fallbackToDestructiveMigration() - .build() - INSTANCE = instance - instance + TODO() +// val instance = Room.databaseBuilder( +// context.applicationContext, +// DogDatabase::class.java, +// dbName +// ).setQueryCallback({ sqlQuery, bindArgs -> floconLogDatabaseQuery( +// dbName = dbName, sqlQuery = sqlQuery, bindArgs = bindArgs +// ) }, Executors.newSingleThreadExecutor()) +// .fallbackToDestructiveMigration() +// .build() +// INSTANCE = instance +// instance } } } diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt index 2afc24fb1..b14bb0625 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt @@ -6,7 +6,7 @@ import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.database.model.FoodEntity import io.github.openflocon.flocon.myapplication.database.model.HumanEntity import io.github.openflocon.flocon.myapplication.database.model.HumanWithDogEntity -import io.github.openflocon.flocon.plugins.database.floconRegisterDatabase +import io.github.openflocon.flocon.pluginsold.database.floconRegisterDatabase import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt deleted file mode 100644 index b456c7acd..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.openflocon.flocon.myapplication.deeplinks - -import io.github.openflocon.flocon.Flocon -import io.github.openflocon.flocon.plugins.deeplinks.deeplinks - -fun initializeDeeplinks() { - Flocon.deeplinks { - variable("test_variable") - variable("host") { - description = "Host variable" - autoComplete(listOf("flocon", "flocon2", "flocon3")) - } - deeplink("[host]://home") { - "host" withVariable "host" - } - deeplink("[host]://test") { - "host" withVariable "host" - } - deeplink("[host]://user/[userId]") { - label = "User" - "userId" withAutoComplete listOf("Florent", "David", "Guillaume") - "host" withVariable "host" - } - deeplink("[host]://post/[postId]?comment=[commentText]") { - label = "Post" - description = "Open a post and send a comment" - "commentText" withVariable "test_variable" - "host" withVariable "host" - } - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/Datastores.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/Datastores.kt index 4b5fab96c..d6aefeec9 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/Datastores.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/Datastores.kt @@ -5,8 +5,6 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import io.github.openflocon.flocon.plugins.sharedprefs.floconRegisterPreference -import io.github.openflocon.flocon.preferences.datastores.model.FloconDatastorePreference import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -38,7 +36,7 @@ class Datastores(private val context: Context) { } init { - floconRegisterPreference(FloconDatastorePreference("datastore", context.dataStore)) + //floconRegisterPreference(FloconDatastorePreference("datastore", context.dataStore)) GlobalScope.launch { saveUsername("John Doe") saveUserAge(30) diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt index 681d24d91..051be10c0 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt @@ -3,12 +3,9 @@ package io.github.openflocon.flocon.myapplication.sharedpreferences import android.content.Context -import android.content.SharedPreferences import android.preference.PreferenceManager import androidx.core.content.edit -import io.github.openflocon.flocon.plugins.sharedprefs.FloconSharedPreference -import io.github.openflocon.flocon.plugins.sharedprefs.floconRegisterPreference -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference +import io.github.openflocon.flocon.pluginsold.sharedprefs.FloconSharedPreference import org.json.JSONArray import org.json.JSONObject import kotlin.uuid.Uuid @@ -17,7 +14,7 @@ import kotlin.uuid.ExperimentalUuidApi fun initializeSharedPreferencesAfterInit(context: Context) { val referencedPref = context.getSharedPreferences("ref_pref", Context.MODE_PRIVATE) - floconRegisterPreference(FloconSharedPreference("my_custom_name", referencedPref)) + //floconRegisterPreference(FloconSharedPreference("my_custom_name", referencedPref)) referencedPref.edit { putString("works", "yes") diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeDashboard.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeDashboard.kt index 9784e3f66..7a5050f0c 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeDashboard.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeDashboard.kt @@ -1,14 +1,11 @@ package io.github.openflocon.flocon.myapplication.table import android.content.Context -import io.github.openflocon.flocon.Flocon -import io.github.openflocon.flocon.plugins.tables.model.toParam -import io.github.openflocon.flocon.plugins.tables.table fun initializeTable(context: Context) { - Flocon.table("analytics").log( - "name" toParam "nameValue", - "value1" toParam "value1Value", - "value2" toParam "value2Value", - ) +// Flocon.table("analytics").log( +// "name" toParam "nameValue", +// "value1" toParam "value1Value", +// "value2" toParam "value2Value", +// ) } \ No newline at end of file diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index e2aa52888..1cad46fc0 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -34,7 +34,6 @@ kotlin { val commonMain by getting { dependencies { implementation(project(":flocon")) - implementation(project(":flocon-base")) implementation(project(":ktor-interceptor")) implementation(libs.kotlinx.coroutines.core) diff --git a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt index 5cf459641..3ad3d762a 100644 --- a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt +++ b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt @@ -10,17 +10,12 @@ import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.ktor.FloconKtorPlugin import io.github.openflocon.flocon.myapplication.multi.Databases.getDogDatabase import io.github.openflocon.flocon.myapplication.multi.Databases.getFoodDatabase -import io.github.openflocon.flocon.myapplication.multi.database.FoodDatabase import io.github.openflocon.flocon.myapplication.multi.database.initializeDatabases -import io.github.openflocon.flocon.myapplication.multi.database.model.DogEntity import io.github.openflocon.flocon.myapplication.multi.sharedpreferences.initializeSharedPreferences import io.github.openflocon.flocon.myapplication.multi.ui.App import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks -import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinksPlugin import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index a0cd76fd5..4b3e4db49 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -14,10 +14,10 @@ dependencyResolutionManagement { } } -rootProject.name = "My Application" +rootProject.name = "Flocon Sample App" + include(":sample-android-only") include(":sample-multiplatform") -include(":flocon-base") include(":flocon") include(":flocon-no-op") include(":okhttp-interceptor") From dd9f914bcf93b831237a52cd88be8e590a96f2cc Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Wed, 11 Mar 2026 23:32:11 +0100 Subject: [PATCH 03/38] feat: Deeplinks --- .../sample-android-only/build.gradle.kts | 4 +++ .../flocon/myapplication/MainActivity.kt | 31 ++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index 562891197..c8dc04fa9 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -83,6 +83,10 @@ dependencies { } else { debugImplementation(project(":flocon")) releaseImplementation(project(":flocon-no-op")) + + debugImplementation(project(":deeplinks")) + releaseImplementation(project(":deeplinks-no-op")) + debugImplementation(project(":okhttp-interceptor")) releaseImplementation(project(":okhttp-interceptor-no-op")) implementation(project(":grpc:grpc-interceptor-lite")) diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index ce99fa234..3adc8f327 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -27,6 +27,7 @@ import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.grpc.GrpcController import io.github.openflocon.flocon.myapplication.ui.ImagesListView import io.github.openflocon.flocon.myapplication.ui.theme.MyApplicationTheme +import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks import io.github.openflocon.flocon.startFlocon import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -212,21 +213,21 @@ class MainActivity : ComponentActivity() { private fun initFlocon() { startFlocon(FloconContext(this)) { -// install(FloconDeeplinks) { -// register("flocon://home") -// register("flocon://test") -// register( -// "flocon://user/[userId]", -// label = "User" -// ) { -// param("userId", listOf("Florent", "David", "Guillaume")) -// } -// register( -// "flocon://post/[postId]?comment=[commentText]", -// label = "Post", -// description = "Open a post and send a comment" -// ) -// } + install(FloconDeeplinks) { + register("flocon://home") + register("flocon://test") + register( + "flocon://user/[userId]", + label = "User" + ) { + param("userId", listOf("Florent", "David", "Guillaume")) + } + register( + "flocon://post/[postId]?comment=[commentText]", + label = "Post", + description = "Open a post and send a comment" + ) + } } } From 5fc6ab7c6c3e2f708fa348a56f656cd5b0693fae Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Wed, 11 Mar 2026 23:50:34 +0100 Subject: [PATCH 04/38] feat: Need to fix install method --- .../plugins/deeplinks/FloconDeeplinks.kt | 3 +- .../io/github/openflocon/flocon/Flocon.kt | 22 ++--- .../io/github/openflocon/flocon/Flocon.kt | 82 ++++++++++++++++--- .../openflocon/flocon/FloconConfiguration.kt | 34 +++++++- .../github/openflocon/flocon/FloconPlugin.kt | 2 +- .../analytics/FloconAnalyticsPlugin.kt | 2 +- .../FloconCrashReporterPlugin.kt | 5 +- .../dashboard/FloconDashboardPlugin.kt | 2 +- .../plugins/database/FloconDatabasePlugin.kt | 2 +- .../plugins/device/FloconDevicePluginImpl.kt | 2 +- .../flocon/plugins/files/FloconFilesPlugin.kt | 2 +- .../network/FloconNetworkPluginImpl.kt | 2 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 2 +- .../plugins/tables/FloconTablesPlugin.kt | 2 +- .../analytics/FloconAnalyticsPlugin.kt | 5 +- .../database/FloconDatabasePlugin.kt | 5 +- .../pluginsold/device/FloconDevicePlugin.kt | 5 +- .../pluginsold/files/FloconFilesPlugin.kt | 5 +- .../pluginsold/network/FloconNetworkPlugin.kt | 5 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 5 +- .../pluginsold/tables/FloconTablesPlugin.kt | 5 +- 21 files changed, 129 insertions(+), 70 deletions(-) diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index 3e2c697ea..3a33d6cae 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -10,7 +10,8 @@ object FloconDeeplinks : FloconPluginFactory Unit = {}) { - val configuration = FloconConfiguration().apply(block) - super.initializeFlocon( - context = FloconContext(context = context), - configuration = configuration - ) - } - -} +//object Flocon : FloconCore() { +// +// fun initialize(context: Context, block: FloconConfiguration.() -> Unit = {}) { +// val configuration = FloconConfiguration().apply(block) +// super.initializeFlocon( +// context = FloconContext(context = context), +// configuration = configuration +// ) +// } +// +//} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt index 411529ea7..97392a96f 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -1,16 +1,72 @@ package io.github.openflocon.flocon +import io.github.openflocon.flocon.FloconApp.Client import io.github.openflocon.flocon.client.FloconClientImpl -// -//internal class Flocon( -// private val context: FloconContext, -// private val plugins: List -//) { -// -// private val client = FloconClientImpl( -// context = context, -// configuration = FloconConfiguration(), -// plugins = plugins -// ) -// -//} \ No newline at end of file +import io.github.openflocon.flocon.core.FloconMessageSender +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal class Flocon( + private val context: FloconContext, + private val plugins: List +) { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + private val client = FloconClientImpl( + context = context, + configuration = FloconConfiguration(), + plugins = plugins + ) + + init { + scope.launch { + start( + client = client, + context = context, + ) + } + } + + private suspend fun start(client: Client, context: FloconContext) { + // try to connect, it fail : try again in 3s + try { + client.connect( + onClosed = { + println("Client - Closed") + // try again to connect + scope.launch { + start( + client = client, + context = context, + ) + } + } + ) + (client as? FloconMessageSender)?.let { + // if success, just send a bonjour + it.send("bonjour", method = "bonjour", body = "bonjour") + it.sendPendingMessages() + } + } catch (t: Throwable) { + if(t.message?.contains("CLEARTEXT communication to localhost not permitted by network security policy") == true) { + withContext(Dispatchers.Main) { + displayClearTextError(context = context) + } + } else { + //t.printStackTrace() + delay(3_000) + start( + client = client, + context = context, + ) + } + } + } + +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index 993c9dce3..9f907afae 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -1,8 +1,13 @@ package io.github.openflocon.flocon +import io.github.openflocon.flocon.client.FloconClientImpl +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + class FloconConfiguration internal constructor() { - internal val pluginConfigs = mutableMapOf, Any>() + internal val pluginConfigs = mutableMapOf, Any>() /** * Install a plugin with the given [factory] and optional [configure] block. @@ -13,13 +18,34 @@ class FloconConfiguration internal constructor() { ) { val config = factory.createConfig() config.configure() - pluginConfigs[factory] = config + pluginConfigs[factory as FloconPluginFactory] = config // TODO } } fun startFlocon(context: FloconContext, block: FloconConfiguration.() -> Unit) { val configuration = FloconConfiguration().apply(block) - - + + Flocon( + context = context, + plugins = configuration.pluginConfigs.map { (factory, config) -> + factory.install( + config, + DumpObject( + FloconClientImpl(context, configuration, plugins = emptyList()) + ) + ) as FloconPlugin + } + ) +} + +class DumpObject( + client: Client +) : FloconApp() { + + override val client: Client = client + + private val _initialized = MutableStateFlow(false) + override val isInitialized: StateFlow = _initialized.asStateFlow() + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt index 3a05b5b0b..b3e8e9177 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -36,5 +36,5 @@ interface FloconPluginFactory : FloconPlugin /** * Install the plugin into the [io.github.openflocon.flocon.FloconApp] instance with the given [config]. */ - fun install(config: Config, app: FloconApp): PluginInstance + fun install(config: Any, app: FloconApp): PluginInstance // TODO } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index 91e19704f..2469db2b4 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -15,7 +15,7 @@ object FloconAnalytics : FloconPluginFactory { override val name: String = "Files" override val pluginId: String = Protocol.ToDevice.Files.Plugin override fun createConfig() = FloconFilesConfig() - override fun install(config: FloconFilesConfig, app: FloconApp): FloconFilesPlugin { + override fun install(config: Any, app: FloconApp): FloconFilesPlugin { val client = app.client return FloconFilesPluginImpl( context = app.context, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt index b6d4d9048..25c107c77 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt @@ -35,7 +35,7 @@ object FloconNetwork : FloconPluginFactory { override fun createConfig(): FloconAnalyticsConfig = TODO() - override fun install( - config: FloconAnalyticsConfig, - app: FloconApp - ): FloconAnalyticsPlugin = TODO() + override fun install(config: Any, app: FloconApp): FloconAnalyticsPlugin = TODO() override val name: String = "" } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt index 36a04ae60..95313a4a8 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt @@ -16,10 +16,7 @@ object FloconDatabase : FloconPluginFactory { TODO("Not yet implemented") } - override fun install( - config: FloconFilesConfig, - app: FloconApp - ): FloconFilesPlugin { + override fun install(config: Any, app: FloconApp): FloconFilesPlugin { TODO("Not yet implemented") } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt index f498f2255..7b23222fa 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt @@ -21,10 +21,7 @@ class FloconNetworkConfig { */ object FloconNetwork : FloconPluginFactory { override fun createConfig(): FloconNetworkConfig = TODO() - override fun install( - config: FloconNetworkConfig, - app: FloconApp - ): FloconNetworkPlugin = TODO() + override fun install(config: Any, app: FloconApp): FloconNetworkPlugin = TODO() override val name: String = "" } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt index 111ff7278..91324e681 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt @@ -14,10 +14,7 @@ object FloconPreferences : FloconPluginFactory { TODO("Not yet implemented") } - override fun install( - config: FloconTableConfig, - app: FloconApp - ): FloconTablePlugin { + override fun install(config: Any, app: FloconApp): FloconTablePlugin { TODO("Not yet implemented") } From fd3457fc25750dfb70a13568808cd4b8aed575b9 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 10:54:14 +0100 Subject: [PATCH 05/38] feat: Make deeplink works --- .../plugins/deeplinks/FloconDeeplinks.kt | 18 +++- .../io/github/openflocon/flocon/Flocon.kt | 36 ++++--- .../io/github/openflocon/flocon/FloconApp.kt | 25 +---- .../openflocon/flocon/FloconConfiguration.kt | 15 ++- .../io/github/openflocon/flocon/FloconCore.kt | 3 +- .../flocon/client/FloconClientImpl.kt | 100 +++++++++++++++++- 6 files changed, 154 insertions(+), 43 deletions(-) diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index 3a33d6cae..ca4dab195 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -1,6 +1,10 @@ package io.github.openflocon.flocon.plugins.deeplinks -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel import kotlinx.coroutines.flow.MutableStateFlow @@ -11,6 +15,7 @@ object FloconDeeplinks : FloconPluginFactory) { - this.deeplinks.update { - deeplinks - } + this.deeplinks.update { deeplinks } try { + println("Deeplinks: sending") sender.send( plugin = Protocol.FromDevice.Deeplink.Plugin, method = Protocol.FromDevice.Deeplink.Method.GetDeeplinks, body = toDeeplinksJson(deeplinks) ) + println("Deeplinks: sent") } catch (t: Throwable) { + println("Deeplinks: error: ${t.message}") + t.printStackTrace() FloconLogger.logError("deeplink mapping error", t) } } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt index 97392a96f..bd1df5393 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -1,29 +1,23 @@ package io.github.openflocon.flocon import io.github.openflocon.flocon.FloconApp.Client -import io.github.openflocon.flocon.client.FloconClientImpl +import io.github.openflocon.flocon.client.FloconClient import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.model.floconMessageFromServerFromJson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext internal class Flocon( private val context: FloconContext, + private val scope: CoroutineScope, + private val client: FloconClient, private val plugins: List ) { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - private val client = FloconClientImpl( - context = context, - configuration = FloconConfiguration(), - plugins = plugins - ) - init { scope.launch { start( @@ -46,20 +40,24 @@ internal class Flocon( context = context, ) } - } + }, + onMessageReceived = ::onMessageReceived ) + + plugins.forEach(FloconPlugin::onConnectedToServer) + (client as? FloconMessageSender)?.let { // if success, just send a bonjour it.send("bonjour", method = "bonjour", body = "bonjour") it.sendPendingMessages() } } catch (t: Throwable) { - if(t.message?.contains("CLEARTEXT communication to localhost not permitted by network security policy") == true) { + if (t.message?.contains("CLEARTEXT communication to localhost not permitted by network security policy") == true) { withContext(Dispatchers.Main) { displayClearTextError(context = context) } } else { - //t.printStackTrace() + t.printStackTrace() delay(3_000) start( client = client, @@ -69,4 +67,16 @@ internal class Flocon( } } + private fun onMessageReceived(message: String) { + scope.launch(Dispatchers.IO) { + floconMessageFromServerFromJson(message)?.let { messageFromServer -> + plugins.find { it.key == messageFromServer.plugin } + ?.onMessageReceived( + method = messageFromServer.method, + body = messageFromServer.body, + ) + } + } + } + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt index 4319f3dab..176c16416 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt @@ -13,32 +13,17 @@ abstract class FloconApp { interface Client { @Throws(Throwable::class) - suspend fun connect(onClosed: () -> Unit) + suspend fun connect( + onClosed: () -> Unit, + onMessageReceived: (message: String) -> Unit + ) + suspend fun disconnect() -// val databasePlugin: FloconDatabasePlugin? -// val dashboardPlugin: FloconDashboardPlugin? -// val tablePlugin: FloconTablePlugin? - //val deeplinksPlugin: FloconDeeplinksPlugin? -// val analyticsPlugin: FloconAnalyticsPlugin? -// val networkPlugin: FloconNetworkPlugin? -// val devicePlugin: FloconDevicePlugin? -// val preferencesPlugin: FloconPreferencesPlugin? -// val crashReporterPlugin: FloconCrashReporterPlugin? - - /** - * Retrieve a plugin instance by its [key]. - */ - fun getPlugin(key: String): T? } open val client: Client? = null abstract val isInitialized: StateFlow -// protected fun initializeFlocon(context: FloconContext) { -// this.context = context -// instance = this -// } - } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index 9f907afae..aa2dca357 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -1,6 +1,11 @@ package io.github.openflocon.flocon +import io.github.openflocon.flocon.client.FloconClient import io.github.openflocon.flocon.client.FloconClientImpl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -25,15 +30,17 @@ class FloconConfiguration internal constructor() { fun startFlocon(context: FloconContext, block: FloconConfiguration.() -> Unit) { val configuration = FloconConfiguration().apply(block) + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val client = FloconClient(context = context, scope = scope) Flocon( context = context, + scope = scope, + client = client, plugins = configuration.pluginConfigs.map { (factory, config) -> factory.install( - config, - DumpObject( - FloconClientImpl(context, configuration, plugins = emptyList()) - ) + config = config, + app = DumpObject(client) // TODO Change ) as FloconPlugin } ) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt index 871f74f61..237f66188 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt @@ -61,7 +61,8 @@ abstract class FloconCore : FloconApp() { context = context, ) } - } + }, + onMessageReceived = {} ) (client as? FloconMessageSender)?.let { // if success, just send a bonjour diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt index 1fe2a7159..d82e71971 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt @@ -35,13 +35,14 @@ internal class FloconClientImpl( private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - override fun getPlugin(key: String): T? { + fun getPlugin(key: String): T? { return plugins.find { it.key == key } as? T } @Throws(Throwable::class) override suspend fun connect( onClosed: () -> Unit, + onMessageReceived: (message: String) -> Unit ) { webSocketClient.connect( address = address, @@ -116,4 +117,101 @@ internal class FloconClientImpl( webSocketClient.sendPendingMessages() } } +} + +internal class FloconClient( + private val context: FloconContext, + private val scope: CoroutineScope +) : FloconApp.Client, FloconMessageSender, FloconFileSender { + + private val appInstance by lazy { currentTimeMillis() } + private val appInfos by lazy { getAppInfos(context) } + private val versionName by lazy { BuildConfig.APP_VERSION } + private val address by lazy { getServerHost(context) } + + private val webSocketClient: FloconWebSocketClient = buildFloconWebSocketClient() + private val httpClient: FloconHttpClient = buildFloconHttpClient() + + @Throws(Throwable::class) + override suspend fun connect( + onClosed: () -> Unit, + onMessageReceived: (message: String) -> Unit + ) { + webSocketClient.connect( + address = address, + port = FLOCON_WEBSOCKET_PORT, + onMessageReceived = ::onMessageReceived, + onClosed = onClosed, + ) + } + + override suspend fun disconnect() { + webSocketClient.disconnect() + } + + private fun onMessageReceived(message: String) { + scope.launch(Dispatchers.IO) { + floconMessageFromServerFromJson(message)?.let { messageFromServer -> + messageFromServer.plugin +// plugins.find { it.key == messageFromServer.plugin } +// ?.onMessageReceived( +// method = messageFromServer.method, +// body = messageFromServer.body, +// ) + } + } + } + + override fun send( + plugin: String, + method: String, + body: String, + ) { + scope.launch(Dispatchers.IO) { + webSocketClient.sendMessage( + message = FloconMessageToServer( + deviceId = appInfos.deviceId, + plugin = plugin, + body = body, + appName = appInfos.appName, + appPackageName = appInfos.appPackageName, + method = method, + deviceName = appInfos.deviceName, + appInstance = appInstance, + platform = appInfos.platform, + versionName = versionName, + ) + .toFloconMessageToServer(), + ) + } + } + + override fun send( + file: FloconFile, + infos: FloconFileInfo, + ) { + scope.launch(Dispatchers.IO) { + httpClient.send( + address = address, + port = FLOCON_HTTP_PORT, + file = file, + infos = infos, + + deviceId = appInfos.deviceId, + appPackageName = appInfos.appPackageName, + appInstance = appInstance, + ) + } + } + + override fun sendPendingMessages() { + scope.launch(Dispatchers.IO) { + webSocketClient.sendPendingMessages() + } + } + + companion object { + private const val FLOCON_WEBSOCKET_PORT = 9023 + private const val FLOCON_HTTP_PORT = 9024 + } } \ No newline at end of file From 6ab9f26969cbb2c5a717b648539c802a8ecf5b5c Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 11:52:50 +0100 Subject: [PATCH 06/38] feat: Clean --- .../plugins/deeplinks/FloconDeeplinks.kt | 28 +--- .../deeplinks/FloconDeeplinksPlugin.kt | 1 + .../io/github/openflocon/flocon/Flocon.kt | 2 +- .../openflocon/flocon/FloconConfiguration.kt | 32 ++-- .../io/github/openflocon/flocon/FloconCore.kt | 3 +- .../github/openflocon/flocon/FloconPlugin.kt | 12 +- .../flocon/client/FloconClientImpl.kt | 156 ++++++++---------- .../flocon/core/FloconFileSender.kt | 2 +- .../flocon/core/FloconMessageSender.kt | 4 +- .../analytics/FloconAnalyticsPlugin.kt | 16 +- .../FloconCrashReporterPlugin.kt | 16 +- .../dashboard/FloconDashboardPlugin.kt | 6 +- .../plugins/database/FloconDatabasePlugin.kt | 52 +++--- .../plugins/device/FloconDevicePluginImpl.kt | 26 +-- .../flocon/plugins/files/FloconFilesPlugin.kt | 36 ++-- .../network/FloconNetworkPluginImpl.kt | 26 +-- .../sharedprefs/FloconSharedPrefsPlugin.kt | 6 +- .../plugins/tables/FloconTablesPlugin.kt | 16 +- .../analytics/FloconAnalyticsPlugin.kt | 5 +- .../FloconCrashReporterPlugin.kt | 3 +- .../dashboard/FloconDashboardPlugin.kt | 3 +- .../database/FloconDatabasePlugin.kt | 5 +- .../pluginsold/device/FloconDevicePlugin.kt | 4 +- .../pluginsold/files/FloconFilesPlugin.kt | 9 +- .../pluginsold/network/FloconNetworkPlugin.kt | 5 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 9 +- .../pluginsold/tables/FloconTablesPlugin.kt | 5 +- .../io/github/openflocon/flocon/ktor/Mocks.kt | 9 + 28 files changed, 248 insertions(+), 249 deletions(-) diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index ca4dab195..2e9e69375 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -14,28 +14,23 @@ object FloconDeeplinks : FloconPluginFactory, private val sender: FloconMessageSender, ) : FloconPlugin, FloconDeeplinksPlugin { override val key: String = "DEEP_LINK" - private val deeplinks = MutableStateFlow?>(null) - - override fun onMessageReceived( + override suspend fun onMessageReceived( method: String, body: String, ) { @@ -43,17 +38,12 @@ internal class FloconDeeplinksPluginImpl( // no op } - override fun onConnectedToServer() { - println("Deeplinks: connected (${deeplinks.value})") - // on connected, send known deeplinks - deeplinks.value?.let { - registerDeeplinks(it) - } + override suspend fun onConnectedToServer() { + println("Deeplinks: connected (${deeplinks})") + registerDeeplinks(deeplinks) } - override fun registerDeeplinks(deeplinks: List) { - this.deeplinks.update { deeplinks } - + override suspend fun registerDeeplinks(deeplinks: List) { try { println("Deeplinks: sending") sender.send( diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index 1e4f57cbd..337106b29 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.plugins.deeplinks import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel class DeeplinkLinkBuilder internal constructor( diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt index bd1df5393..31eab3f3d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -44,7 +44,7 @@ internal class Flocon( onMessageReceived = ::onMessageReceived ) - plugins.forEach(FloconPlugin::onConnectedToServer) + plugins.forEach { it.onConnectedToServer() } (client as? FloconMessageSender)?.let { // if success, just send a bonjour diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index aa2dca357..e1253bf15 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -1,7 +1,6 @@ package io.github.openflocon.flocon import io.github.openflocon.flocon.client.FloconClient -import io.github.openflocon.flocon.client.FloconClientImpl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -10,39 +9,40 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -class FloconConfiguration internal constructor() { +class FloconConfiguration internal constructor( + private val client: FloconClient +) { - internal val pluginConfigs = mutableMapOf, Any>() + internal val plugins = mutableListOf() /** * Install a plugin with the given [factory] and optional [configure] block. */ - fun install( - factory: FloconPluginFactory, + fun install( + factory: FloconPluginFactory, configure: Config.() -> Unit = {} ) { - val config = factory.createConfig() - config.configure() - pluginConfigs[factory as FloconPluginFactory] = config // TODO + val plugin = factory.install( + config = factory.createConfig() + .apply { configure() }, + app = DumpObject(client = client) + ) + + plugins.add(plugin) } } fun startFlocon(context: FloconContext, block: FloconConfiguration.() -> Unit) { - val configuration = FloconConfiguration().apply(block) val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - val client = FloconClient(context = context, scope = scope) + val client = FloconClient(context = context) + val configuration = FloconConfiguration(client = client).apply(block) Flocon( context = context, scope = scope, client = client, - plugins = configuration.pluginConfigs.map { (factory, config) -> - factory.install( - config = config, - app = DumpObject(client) // TODO Change - ) as FloconPlugin - } + plugins = configuration.plugins ) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt index 237f66188..be8a0c673 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt @@ -1,6 +1,5 @@ package io.github.openflocon.flocon -import io.github.openflocon.flocon.client.FloconClientImpl import io.github.openflocon.flocon.core.FloconMessageSender import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -30,7 +29,7 @@ abstract class FloconCore : FloconApp() { protected fun initializeFlocon( context: FloconContext, - configuration: FloconConfiguration = FloconConfiguration() + configuration: FloconConfiguration = FloconConfiguration(TODO()) ) { //super.initializeFlocon(context) // val newClient = FloconClientImpl(context, configuration) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt index b3e8e9177..53865e802 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -7,14 +7,16 @@ package io.github.openflocon.flocon interface FloconPlugin { val key: String - fun onMessageReceived( + suspend fun onMessageReceived( method: String, body: String, ) - fun onConnectedToServer() + suspend fun onConnectedToServer() } +interface FloconPluginConfig + /** * A unique key for identifying a Flocon plugin. */ @@ -27,7 +29,8 @@ interface FloconPluginKey { * A factory for creating and installing Flocon plugins. * This is the entry point for Ktor-style [install] calls. */ -interface FloconPluginFactory : FloconPluginKey { +interface FloconPluginFactory : FloconPluginKey { + /** * Create a default configuration instance for the plugin. */ @@ -36,5 +39,6 @@ interface FloconPluginFactory : FloconPlugin /** * Install the plugin into the [io.github.openflocon.flocon.FloconApp] instance with the given [config]. */ - fun install(config: Any, app: FloconApp): PluginInstance // TODO + fun install(config: Config, app: FloconApp): PluginInstance // TODO + } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt index d82e71971..7c7b58a5b 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/client/FloconClientImpl.kt @@ -1,8 +1,18 @@ package io.github.openflocon.flocon.client -import io.github.openflocon.flocon.* -import io.github.openflocon.flocon.core.* -import io.github.openflocon.flocon.model.* +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconConfiguration +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.core.FloconFileSender +import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.getAppInfos +import io.github.openflocon.flocon.getServerHost +import io.github.openflocon.flocon.model.FloconFileInfo +import io.github.openflocon.flocon.model.FloconMessageToServer +import io.github.openflocon.flocon.model.floconMessageFromServerFromJson +import io.github.openflocon.flocon.model.toFloconMessageToServer import io.github.openflocon.flocon.utils.currentTimeMillis import io.github.openflocon.flocon.websocket.FloconHttpClient import io.github.openflocon.flocon.websocket.FloconWebSocketClient @@ -50,7 +60,7 @@ internal class FloconClientImpl( onMessageReceived = ::onMessageReceived, onClosed = onClosed, ) - plugins.forEach(FloconPlugin::onConnectedToServer) + plugins.forEach { it.onConnectedToServer() } } override suspend fun disconnect() { @@ -70,58 +80,51 @@ internal class FloconClientImpl( } } - override fun send( + override suspend fun send( plugin: String, method: String, body: String, ) { - coroutineScope.launch(Dispatchers.IO) { - webSocketClient.sendMessage( - message = FloconMessageToServer( - deviceId = appInfos.deviceId, - plugin = plugin, - body = body, - appName = appInfos.appName, - appPackageName = appInfos.appPackageName, - method = method, - deviceName = appInfos.deviceName, - appInstance = appInstance, - platform = appInfos.platform, - versionName = versionName, - ) - .toFloconMessageToServer(), + webSocketClient.sendMessage( + message = FloconMessageToServer( + deviceId = appInfos.deviceId, + plugin = plugin, + body = body, + appName = appInfos.appName, + appPackageName = appInfos.appPackageName, + method = method, + deviceName = appInfos.deviceName, + appInstance = appInstance, + platform = appInfos.platform, + versionName = versionName, ) - } + .toFloconMessageToServer(), + ) } - override fun send( + override suspend fun send( file: FloconFile, infos: FloconFileInfo, ) { - coroutineScope.launch(Dispatchers.IO) { - httpClient.send( - address = address, - port = FLOCON_HTTP_PORT, - file = file, - infos = infos, + httpClient.send( + address = address, + port = FLOCON_HTTP_PORT, + file = file, + infos = infos, - deviceId = appInfos.deviceId, - appPackageName = appInfos.appPackageName, - appInstance = appInstance, - ) - } + deviceId = appInfos.deviceId, + appPackageName = appInfos.appPackageName, + appInstance = appInstance, + ) } - override fun sendPendingMessages() { - coroutineScope.launch(Dispatchers.IO) { - webSocketClient.sendPendingMessages() - } + override suspend fun sendPendingMessages() { + webSocketClient.sendPendingMessages() } } internal class FloconClient( - private val context: FloconContext, - private val scope: CoroutineScope + private val context: FloconContext ) : FloconApp.Client, FloconMessageSender, FloconFileSender { private val appInstance by lazy { currentTimeMillis() } @@ -140,7 +143,7 @@ internal class FloconClient( webSocketClient.connect( address = address, port = FLOCON_WEBSOCKET_PORT, - onMessageReceived = ::onMessageReceived, + onMessageReceived = onMessageReceived, onClosed = onClosed, ) } @@ -149,65 +152,46 @@ internal class FloconClient( webSocketClient.disconnect() } - private fun onMessageReceived(message: String) { - scope.launch(Dispatchers.IO) { - floconMessageFromServerFromJson(message)?.let { messageFromServer -> - messageFromServer.plugin -// plugins.find { it.key == messageFromServer.plugin } -// ?.onMessageReceived( -// method = messageFromServer.method, -// body = messageFromServer.body, -// ) - } - } - } - - override fun send( + override suspend fun send( plugin: String, method: String, body: String, ) { - scope.launch(Dispatchers.IO) { - webSocketClient.sendMessage( - message = FloconMessageToServer( - deviceId = appInfos.deviceId, - plugin = plugin, - body = body, - appName = appInfos.appName, - appPackageName = appInfos.appPackageName, - method = method, - deviceName = appInfos.deviceName, - appInstance = appInstance, - platform = appInfos.platform, - versionName = versionName, - ) - .toFloconMessageToServer(), + webSocketClient.sendMessage( + message = FloconMessageToServer( + deviceId = appInfos.deviceId, + plugin = plugin, + body = body, + appName = appInfos.appName, + appPackageName = appInfos.appPackageName, + method = method, + deviceName = appInfos.deviceName, + appInstance = appInstance, + platform = appInfos.platform, + versionName = versionName, ) - } + .toFloconMessageToServer(), + ) } - override fun send( + override suspend fun send( file: FloconFile, infos: FloconFileInfo, ) { - scope.launch(Dispatchers.IO) { - httpClient.send( - address = address, - port = FLOCON_HTTP_PORT, - file = file, - infos = infos, + httpClient.send( + address = address, + port = FLOCON_HTTP_PORT, + file = file, + infos = infos, - deviceId = appInfos.deviceId, - appPackageName = appInfos.appPackageName, - appInstance = appInstance, - ) - } + deviceId = appInfos.deviceId, + appPackageName = appInfos.appPackageName, + appInstance = appInstance, + ) } - override fun sendPendingMessages() { - scope.launch(Dispatchers.IO) { - webSocketClient.sendPendingMessages() - } + override suspend fun sendPendingMessages() { + webSocketClient.sendPendingMessages() } companion object { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt index 929fe8411..3f5b977ce 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt @@ -4,5 +4,5 @@ import io.github.openflocon.flocon.FloconFile import io.github.openflocon.flocon.model.FloconFileInfo internal interface FloconFileSender { - fun send(file: FloconFile, infos: FloconFileInfo) + suspend fun send(file: FloconFile, infos: FloconFileInfo) } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt index 4e99fbc91..dbff6b15b 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt @@ -1,11 +1,11 @@ package io.github.openflocon.flocon.core interface FloconMessageSender { - fun send( + suspend fun send( plugin: String, method: String, body: String, ) - fun sendPendingMessages() + suspend fun sendPendingMessages() } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index 2469db2b4..d0b293286 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -15,7 +15,7 @@ object FloconAnalytics : FloconPluginFactory) { analyticsItems.takeIf { it.isNotEmpty() }?.forEach { toSend -> try { - sender.send( - plugin = Protocol.FromDevice.Analytics.Plugin, - method = Protocol.FromDevice.Analytics.Method.AddItems, - body = analyticsItemsToJson(toSend) - ) +// sender.send( +// plugin = Protocol.FromDevice.Analytics.Plugin, +// method = Protocol.FromDevice.Analytics.Method.AddItems, +// body = analyticsItemsToJson(toSend) +// ) } catch (t: Throwable) { FloconLogger.logError("error on sendAnalytics", t) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt index 835814ed5..e230612b1 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt @@ -22,7 +22,7 @@ object FloconCrashReporter : Protocol.ToDevice.Analytics.Plugin // Crash reporter is usually write-only but we can set an ID override fun createConfig() = FloconCrashReporterConfig() - override fun install(config: Any, app: FloconApp): FloconCrashReporterPlugin { + override fun install(config: FloconCrashReporterConfig, app: FloconApp): FloconCrashReporterPlugin { val client = app.client as FloconMessageSender return FloconCrashReporterPluginImpl( context = TODO(), //FloconContext(appContext = null), // Handled by datasource @@ -48,7 +48,7 @@ internal class FloconCrashReporterPluginImpl( } } - override fun onConnectedToServer() { + override suspend fun onConnectedToServer() { // Send all pending crashes coroutineScope.launch { try { @@ -64,7 +64,7 @@ internal class FloconCrashReporterPluginImpl( } } - override fun onMessageReceived( + override suspend fun onMessageReceived( method: String, body: String, ) { @@ -73,11 +73,11 @@ internal class FloconCrashReporterPluginImpl( private fun sendCrashes(crashes: List) { try { - sender.send( - plugin = Protocol.FromDevice.CrashReporter.Plugin, - method = Protocol.FromDevice.CrashReporter.Method.ReportCrash, - body = crashReportsListToJson(crashes), - ) +// sender.send( +// plugin = Protocol.FromDevice.CrashReporter.Plugin, +// method = Protocol.FromDevice.CrashReporter.Method.ReportCrash, +// body = crashReportsListToJson(crashes), +// ) } catch (t: Throwable) { FloconLogger.logError("Crash report sending error", t) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt index 69cd31a9f..018cf0b74 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch override val name: String = "Dashboard" override val pluginId: String = Protocol.ToDevice.Dashboard.Plugin override fun createConfig() = FloconDashboardConfig() - override fun install(config: Any, app: FloconApp): FloconDashboardPlugin { + override fun install(config: FloconDashboardConfig, app: FloconApp): FloconDashboardPlugin { return FloconDashboardPluginImpl( sender = app.client as FloconMessageSender ) @@ -38,7 +38,7 @@ internal class FloconDashboardPluginImpl( private val dashboards = mutableMapOf() private val callbackMap = mutableMapOf() - override fun onMessageReceived( + override suspend fun onMessageReceived( method: String, body: String, ) { @@ -76,7 +76,7 @@ internal class FloconDashboardPluginImpl( } } - override fun onConnectedToServer() { + override suspend fun onConnectedToServer() { // on connected, send known dashboards dashboards.values.takeIf { it.isNotEmpty() }?.forEach { dashboardConfig -> registerDashboardInternal(dashboardConfig) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt index 36ebe85eb..69d662783 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt @@ -38,7 +38,7 @@ object FloconDatabase : FloconPluginFactory) { try { - sender.send( - plugin = Protocol.FromDevice.Database.Plugin, - method = Protocol.FromDevice.Database.Method.LogQuery, - body = DatabaseQueryLogModel( - dbName = dbName, - sqlQuery = sqlQuery, - bindArgs = bindArgs.map { it.toString() }, - timestamp = currentTimeMillis(), - ).toJson(), - ) +// sender.send( +// plugin = Protocol.FromDevice.Database.Plugin, +// method = Protocol.FromDevice.Database.Method.LogQuery, +// body = DatabaseQueryLogModel( +// dbName = dbName, +// sqlQuery = sqlQuery, +// bindArgs = bindArgs.map { it.toString() }, +// timestamp = currentTimeMillis(), +// ).toJson(), +// ) } catch (t: Throwable) { FloconLogger.logError("Database logging error", t) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt index 61fc5317f..ef90efbcc 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt @@ -10,7 +10,7 @@ object FloconDevice : FloconPluginFactory { val icon = getAppIconBase64(context) if (icon != null) { - sender.send( - plugin = Protocol.FromDevice.Device.Plugin, - method = Protocol.FromDevice.Device.Method.AppIcon, - body = icon, - ) +// sender.send( +// plugin = Protocol.FromDevice.Device.Plugin, +// method = Protocol.FromDevice.Device.Method.AppIcon, +// body = icon, +// ) } } @@ -60,7 +60,7 @@ internal class FloconDevicePluginImpl( } } - override fun onConnectedToServer() { + override suspend fun onConnectedToServer() { // no op } } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt index d887edb1e..85b61b199 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt @@ -26,7 +26,7 @@ object FloconFiles : FloconPluginFactory { override val name: String = "Files" override val pluginId: String = Protocol.ToDevice.Files.Plugin override fun createConfig() = FloconFilesConfig() - override fun install(config: Any, app: FloconApp): FloconFilesPlugin { + override fun install(config: FloconFilesConfig, app: FloconApp): FloconFilesPlugin { val client = app.client return FloconFilesPluginImpl( context = app.context, @@ -61,7 +61,7 @@ internal class FloconFilesPluginImpl( private val fileDataSource = fileDataSource(context) private val withFoldersSize = MutableStateFlow(false) - override fun onMessageReceived( + override suspend fun onMessageReceived( method: String, body: String, ) { @@ -83,13 +83,13 @@ internal class FloconFilesPluginImpl( fileDataSource.getFile(path = getFileMessage.path, isConstantPath = false) ?.let { file -> - floconFileSender.send( - file = file, - infos = FloconFileInfo( - requestId = getFileMessage.requestId, - path = getFileMessage.path, - ) - ) +// floconFileSender.send( +// file = file, +// infos = FloconFileInfo( +// requestId = getFileMessage.requestId, +// path = getFileMessage.path, +// ) +// ) } } @@ -158,20 +158,20 @@ internal class FloconFilesPluginImpl( ) try { - sender.send( - plugin = Protocol.FromDevice.Files.Plugin, - method = Protocol.FromDevice.Files.Method.ListFiles, - body = FilesResultDataModel( - requestId = requestId, - files = files, - ).toJson(), - ) +// sender.send( +// plugin = Protocol.FromDevice.Files.Plugin, +// method = Protocol.FromDevice.Files.Method.ListFiles, +// body = FilesResultDataModel( +// requestId = requestId, +// files = files, +// ).toJson(), +// ) } catch (t: Throwable) { FloconLogger.logError("File parsing error", t) } } - override fun onConnectedToServer() { + override suspend fun onConnectedToServer() { // no op } } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt index 25c107c77..6d2a9c0b8 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt @@ -35,7 +35,7 @@ object FloconNetwork : FloconPluginFactory() - override fun onMessageReceived( + override suspend fun onMessageReceived( method: String, body: String, ) { @@ -62,7 +62,7 @@ internal class FloconSharedPrefsPluginImpl( // } } - override fun onConnectedToServer() { + override suspend fun onConnectedToServer() { sendSharedPreferences() } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt index 3c95bc023..2306b0664 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt @@ -11,7 +11,7 @@ import io.github.openflocon.flocon.pluginsold.tables.model.TableItem override val name: String = "Table" override val pluginId: String = Protocol.ToDevice.Table.Plugin override fun createConfig() = FloconTableConfig() - override fun install(config: Any, app: FloconApp): FloconTablePlugin { + override fun install(config: FloconTableConfig, app: FloconApp): FloconTablePlugin { return FloconTablePluginImpl( sender = app.client as FloconMessageSender ) @@ -23,14 +23,14 @@ internal class FloconTablePluginImpl( ) : FloconPlugin, FloconTablePlugin { override val key: String = "TABLE" - override fun onMessageReceived( + override suspend fun onMessageReceived( method: String, body: String, ) { // no op } - override fun onConnectedToServer() { + override suspend fun onConnectedToServer() { // no op } @@ -40,11 +40,11 @@ internal class FloconTablePluginImpl( private fun sendTable(tableItems: List) { try { - sender.send( - plugin = Protocol.FromDevice.Table.Plugin, - method = Protocol.FromDevice.Table.Method.AddItems, - body = tableItemListToJson(tableItems).toString() - ) +// sender.send( +// plugin = Protocol.FromDevice.Table.Plugin, +// method = Protocol.FromDevice.Table.Method.AddItems, +// body = tableItemListToJson(tableItems).toString() +// ) } catch (t: Throwable) { FloconLogger.logError("Table json mapping error", t) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt index b9b3db73d..a64cc3866 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt @@ -2,17 +2,18 @@ package io.github.openflocon.flocon.pluginsold.analytics import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem -class FloconAnalyticsConfig +class FloconAnalyticsConfig : FloconPluginConfig /** * Flocon Analytics Plugin. */ object FloconAnalytics : FloconPluginFactory { override fun createConfig(): FloconAnalyticsConfig = TODO() - override fun install(config: Any, app: FloconApp): FloconAnalyticsPlugin = TODO() + override fun install(config: FloconAnalyticsConfig, app: FloconApp): FloconAnalyticsPlugin = TODO() override val name: String = "" } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt index a4de6eaf9..e30e0037d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt @@ -1,8 +1,9 @@ package io.github.openflocon.flocon.pluginsold.crashreporter import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig -class FloconCrashReporterConfig { +class FloconCrashReporterConfig : FloconPluginConfig { var catchFatalErrors: Boolean = true } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/FloconDashboardPlugin.kt index f0dde5d6a..48aa3c542 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/FloconDashboardPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/dashboard/FloconDashboardPlugin.kt @@ -1,9 +1,10 @@ package io.github.openflocon.flocon.pluginsold.dashboard import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig -class FloconDashboardConfig +class FloconDashboardConfig : FloconPluginConfig /** * Flocon Dashboard Plugin. diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt index 95313a4a8..1f07a69b8 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt @@ -2,10 +2,11 @@ package io.github.openflocon.flocon.pluginsold.database import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.pluginsold.database.model.FloconDatabaseModel -class FloconDatabaseConfig +class FloconDatabaseConfig : FloconPluginConfig /** * Flocon Database Plugin. @@ -16,7 +17,7 @@ object FloconDatabase : FloconPluginFactory() } @@ -15,7 +18,7 @@ object FloconFiles : FloconPluginFactory { TODO("Not yet implemented") } - override fun install(config: Any, app: FloconApp): FloconFilesPlugin { + override fun install(config: FloconFilesConfig, app: FloconApp): FloconFilesPlugin { TODO("Not yet implemented") } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt index 7b23222fa..2f220e3b2 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.pluginsold.network import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest @@ -10,7 +11,7 @@ import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketMockListener import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse -class FloconNetworkConfig { +class FloconNetworkConfig : FloconPluginConfig { var badQualityConfig: BadQualityConfig? = null val mocks = mutableListOf() } @@ -21,7 +22,7 @@ class FloconNetworkConfig { */ object FloconNetwork : FloconPluginFactory { override fun createConfig(): FloconNetworkConfig = TODO() - override fun install(config: Any, app: FloconApp): FloconNetworkPlugin = TODO() + override fun install(config: FloconNetworkConfig, app: FloconApp): FloconNetworkPlugin = TODO() override val name: String = "" } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt index 91324e681..6beb16aa2 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt @@ -1,9 +1,12 @@ package io.github.openflocon.flocon.pluginsold.sharedprefs -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconSharedPreferenceModel -class FloconPreferencesConfig +class FloconPreferencesConfig : FloconPluginConfig /** * Flocon Preferences Plugin. @@ -14,7 +17,7 @@ object FloconPreferences : FloconPluginFactory { TODO("Not yet implemented") } - override fun install(config: Any, app: FloconApp): FloconTablePlugin { + override fun install(config: FloconTableConfig, app: FloconApp): FloconTablePlugin { TODO("Not yet implemented") } diff --git a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt b/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt index 57f0da6a8..d6d99e3b7 100644 --- a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt +++ b/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt @@ -10,6 +10,7 @@ import io.ktor.http.HeadersBuilder import io.ktor.http.HttpProtocolVersion import io.ktor.http.HttpStatusCode import io.ktor.util.date.GMTDate +import io.ktor.util.logging.Logger import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.InternalAPI import kotlinx.coroutines.delay @@ -28,6 +29,14 @@ internal fun findMock( } } +//fun test() { +// HttpClient { +// install() { +// +// } +// } +//} + @OptIn(InternalAPI::class) internal suspend fun executeMock( request: HttpRequestBuilder, From 6c469516de9e1b9600d6df470fec7d830ed15f94 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 13:37:17 +0100 Subject: [PATCH 07/38] feat: Create core network modules --- .../network/core-no-op/build.gradle.kts | 118 +++++++++++++++++ .../src/androidMain/AndroidManifest.xml | 3 + .../network/core/noop/NetworkCoreNoOp.kt | 3 + FloconAndroid/network/core/build.gradle.kts | 121 ++++++++++++++++++ .../core/src/androidMain/AndroidManifest.xml | 3 + .../flocon/network/core/NetworkCore.kt | 3 + .../ktor-interceptor-no-op/.gitignore | 0 .../ktor-interceptor-no-op/build.gradle.kts | 2 +- .../ktor-interceptor-no-op/consumer-rules.pro | 0 .../ktor-interceptor-no-op/proguard-rules.pro | 0 .../flocon/ktor/FloconKtorPlugin.kt | 0 .../{ => network}/ktor-interceptor/.gitignore | 0 .../ktor-interceptor/build.gradle.kts | 2 +- .../ktor-interceptor/consumer-rules.pro | 0 .../ktor-interceptor/proguard-rules.pro | 0 .../openflocon/flocon/ktor/DecodeUtils.kt | 0 .../openflocon/flocon/ktor/BadQuality.kt | 0 .../flocon/ktor/FloconKtorPlugin.kt | 0 .../io/github/openflocon/flocon/ktor/Mocks.kt | 0 .../io/github/openflocon/flocon/ktor/Utils.kt | 0 .../openflocon/flocon/ktor/DecodeUtils.kt | 0 .../openflocon/flocon/ktor/DecodeUtils.kt | 0 .../okhttp-interceptor-no-op/.gitignore | 0 .../okhttp-interceptor-no-op/build.gradle.kts | 1 + .../consumer-rules.pro | 0 .../proguard-rules.pro | 0 .../src/main/AndroidManifest.xml | 0 .../flocon/okhttp/OkHttpInterceptor.kt | 0 .../okhttp/websocket/FloconWebSocket.kt | 0 .../okhttp-interceptor/.gitignore | 0 .../okhttp-interceptor/build.gradle.kts | 2 +- .../okhttp-interceptor/consumer-rules.pro | 0 .../okhttp-interceptor/proguard-rules.pro | 0 .../src/main/AndroidManifest.xml | 0 .../openflocon/flocon/okhttp/BadQuality.kt | 0 .../github/openflocon/flocon/okhttp/Mock.kt | 0 .../flocon/okhttp/OkHttpInterceptor.kt | 0 .../github/openflocon/flocon/okhttp/Utils.kt | 0 .../okhttp/websocket/FloconWebSocket.kt | 0 .../openflocon/flocon/okhttp/UtilsTest.kt | 0 .../sample-android-only/build.gradle.kts | 8 +- .../sample-multiplatform/build.gradle.kts | 2 +- FloconAndroid/settings.gradle.kts | 10 +- 43 files changed, 266 insertions(+), 12 deletions(-) create mode 100644 FloconAndroid/network/core-no-op/build.gradle.kts create mode 100644 FloconAndroid/network/core-no-op/src/androidMain/AndroidManifest.xml create mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/NetworkCoreNoOp.kt create mode 100644 FloconAndroid/network/core/build.gradle.kts create mode 100644 FloconAndroid/network/core/src/androidMain/AndroidManifest.xml create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/NetworkCore.kt rename FloconAndroid/{ => network}/ktor-interceptor-no-op/.gitignore (100%) rename FloconAndroid/{ => network}/ktor-interceptor-no-op/build.gradle.kts (98%) rename FloconAndroid/{ => network}/ktor-interceptor-no-op/consumer-rules.pro (100%) rename FloconAndroid/{ => network}/ktor-interceptor-no-op/proguard-rules.pro (100%) rename FloconAndroid/{ => network}/ktor-interceptor-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt (100%) rename FloconAndroid/{ => network}/ktor-interceptor/.gitignore (100%) rename FloconAndroid/{ => network}/ktor-interceptor/build.gradle.kts (98%) rename FloconAndroid/{ => network}/ktor-interceptor/consumer-rules.pro (100%) rename FloconAndroid/{ => network}/ktor-interceptor/proguard-rules.pro (100%) rename FloconAndroid/{ => network}/ktor-interceptor/src/androidMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt (100%) rename FloconAndroid/{ => network}/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt (100%) rename FloconAndroid/{ => network}/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt (100%) rename FloconAndroid/{ => network}/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt (100%) rename FloconAndroid/{ => network}/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Utils.kt (100%) rename FloconAndroid/{ => network}/ktor-interceptor/src/iosMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt (100%) rename FloconAndroid/{ => network}/ktor-interceptor/src/jvmMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt (100%) rename FloconAndroid/{ => network}/okhttp-interceptor-no-op/.gitignore (100%) rename FloconAndroid/{ => network}/okhttp-interceptor-no-op/build.gradle.kts (97%) rename FloconAndroid/{ => network}/okhttp-interceptor-no-op/consumer-rules.pro (100%) rename FloconAndroid/{ => network}/okhttp-interceptor-no-op/proguard-rules.pro (100%) rename FloconAndroid/{ => network}/okhttp-interceptor-no-op/src/main/AndroidManifest.xml (100%) rename FloconAndroid/{ => network}/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt (100%) rename FloconAndroid/{ => network}/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/.gitignore (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/build.gradle.kts (98%) rename FloconAndroid/{ => network}/okhttp-interceptor/consumer-rules.pro (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/proguard-rules.pro (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/src/main/AndroidManifest.xml (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt (100%) rename FloconAndroid/{ => network}/okhttp-interceptor/src/test/kotlin/io/github/openflocon/flocon/okhttp/UtilsTest.kt (100%) diff --git a/FloconAndroid/network/core-no-op/build.gradle.kts b/FloconAndroid/network/core-no-op/build.gradle.kts new file mode 100644 index 000000000..7d09e14e2 --- /dev/null +++ b/FloconAndroid/network/core-no-op/build.gradle.kts @@ -0,0 +1,118 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "io.github.openflocon.flocon.network.core.noop" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-network-core-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Network Core No-Op" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/network/core-no-op/src/androidMain/AndroidManifest.xml b/FloconAndroid/network/core-no-op/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..9a40236b9 --- /dev/null +++ b/FloconAndroid/network/core-no-op/src/androidMain/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/NetworkCoreNoOp.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/NetworkCoreNoOp.kt new file mode 100644 index 000000000..cf487ea7b --- /dev/null +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/NetworkCoreNoOp.kt @@ -0,0 +1,3 @@ +package io.github.openflocon.flocon.network.core.noop + +// Placeholder for Network Core No-Op diff --git a/FloconAndroid/network/core/build.gradle.kts b/FloconAndroid/network/core/build.gradle.kts new file mode 100644 index 000000000..af58159d7 --- /dev/null +++ b/FloconAndroid/network/core/build.gradle.kts @@ -0,0 +1,121 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + api(project(":flocon")) + + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(libs.kotlinx.serialization.json) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "io.github.openflocon.flocon.network.core" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-network-core", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Network Core" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/network/core/src/androidMain/AndroidManifest.xml b/FloconAndroid/network/core/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..9a40236b9 --- /dev/null +++ b/FloconAndroid/network/core/src/androidMain/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/NetworkCore.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/NetworkCore.kt new file mode 100644 index 000000000..1ce7f0e47 --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/NetworkCore.kt @@ -0,0 +1,3 @@ +package io.github.openflocon.flocon.network.core + +// Placeholder for Network Core diff --git a/FloconAndroid/ktor-interceptor-no-op/.gitignore b/FloconAndroid/network/ktor-interceptor-no-op/.gitignore similarity index 100% rename from FloconAndroid/ktor-interceptor-no-op/.gitignore rename to FloconAndroid/network/ktor-interceptor-no-op/.gitignore diff --git a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts similarity index 98% rename from FloconAndroid/ktor-interceptor-no-op/build.gradle.kts rename to FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts index f645d5790..0f966ba28 100644 --- a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) + implementation(project(":network:core-no-op")) implementation(libs.ktor.client.core) } } diff --git a/FloconAndroid/ktor-interceptor-no-op/consumer-rules.pro b/FloconAndroid/network/ktor-interceptor-no-op/consumer-rules.pro similarity index 100% rename from FloconAndroid/ktor-interceptor-no-op/consumer-rules.pro rename to FloconAndroid/network/ktor-interceptor-no-op/consumer-rules.pro diff --git a/FloconAndroid/ktor-interceptor-no-op/proguard-rules.pro b/FloconAndroid/network/ktor-interceptor-no-op/proguard-rules.pro similarity index 100% rename from FloconAndroid/ktor-interceptor-no-op/proguard-rules.pro rename to FloconAndroid/network/ktor-interceptor-no-op/proguard-rules.pro diff --git a/FloconAndroid/ktor-interceptor-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt b/FloconAndroid/network/ktor-interceptor-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt similarity index 100% rename from FloconAndroid/ktor-interceptor-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt rename to FloconAndroid/network/ktor-interceptor-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt diff --git a/FloconAndroid/ktor-interceptor/.gitignore b/FloconAndroid/network/ktor-interceptor/.gitignore similarity index 100% rename from FloconAndroid/ktor-interceptor/.gitignore rename to FloconAndroid/network/ktor-interceptor/.gitignore diff --git a/FloconAndroid/ktor-interceptor/build.gradle.kts b/FloconAndroid/network/ktor-interceptor/build.gradle.kts similarity index 98% rename from FloconAndroid/ktor-interceptor/build.gradle.kts rename to FloconAndroid/network/ktor-interceptor/build.gradle.kts index d18d78819..0bc55cdc3 100644 --- a/FloconAndroid/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) + implementation(project(":network:core")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") implementation(libs.ktor.client.core) } diff --git a/FloconAndroid/ktor-interceptor/consumer-rules.pro b/FloconAndroid/network/ktor-interceptor/consumer-rules.pro similarity index 100% rename from FloconAndroid/ktor-interceptor/consumer-rules.pro rename to FloconAndroid/network/ktor-interceptor/consumer-rules.pro diff --git a/FloconAndroid/ktor-interceptor/proguard-rules.pro b/FloconAndroid/network/ktor-interceptor/proguard-rules.pro similarity index 100% rename from FloconAndroid/ktor-interceptor/proguard-rules.pro rename to FloconAndroid/network/ktor-interceptor/proguard-rules.pro diff --git a/FloconAndroid/ktor-interceptor/src/androidMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt b/FloconAndroid/network/ktor-interceptor/src/androidMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt similarity index 100% rename from FloconAndroid/ktor-interceptor/src/androidMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt rename to FloconAndroid/network/ktor-interceptor/src/androidMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt diff --git a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt b/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt similarity index 100% rename from FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt rename to FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt diff --git a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt b/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt similarity index 100% rename from FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt rename to FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/FloconKtorPlugin.kt diff --git a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt b/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt similarity index 100% rename from FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt rename to FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt diff --git a/FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Utils.kt b/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Utils.kt similarity index 100% rename from FloconAndroid/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Utils.kt rename to FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Utils.kt diff --git a/FloconAndroid/ktor-interceptor/src/iosMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt b/FloconAndroid/network/ktor-interceptor/src/iosMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt similarity index 100% rename from FloconAndroid/ktor-interceptor/src/iosMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt rename to FloconAndroid/network/ktor-interceptor/src/iosMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt diff --git a/FloconAndroid/ktor-interceptor/src/jvmMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt b/FloconAndroid/network/ktor-interceptor/src/jvmMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt similarity index 100% rename from FloconAndroid/ktor-interceptor/src/jvmMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt rename to FloconAndroid/network/ktor-interceptor/src/jvmMain/kotlin/io/github/openflocon/flocon/ktor/DecodeUtils.kt diff --git a/FloconAndroid/okhttp-interceptor-no-op/.gitignore b/FloconAndroid/network/okhttp-interceptor-no-op/.gitignore similarity index 100% rename from FloconAndroid/okhttp-interceptor-no-op/.gitignore rename to FloconAndroid/network/okhttp-interceptor-no-op/.gitignore diff --git a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts b/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts similarity index 97% rename from FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts rename to FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts index 897fd4fc9..739f6b1ee 100644 --- a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts @@ -34,6 +34,7 @@ android { } dependencies { + implementation(project(":network:core-no-op")) implementation(platform(libs.okhttp.bom)) implementation(libs.okhttp3.okhttp) } diff --git a/FloconAndroid/okhttp-interceptor-no-op/consumer-rules.pro b/FloconAndroid/network/okhttp-interceptor-no-op/consumer-rules.pro similarity index 100% rename from FloconAndroid/okhttp-interceptor-no-op/consumer-rules.pro rename to FloconAndroid/network/okhttp-interceptor-no-op/consumer-rules.pro diff --git a/FloconAndroid/okhttp-interceptor-no-op/proguard-rules.pro b/FloconAndroid/network/okhttp-interceptor-no-op/proguard-rules.pro similarity index 100% rename from FloconAndroid/okhttp-interceptor-no-op/proguard-rules.pro rename to FloconAndroid/network/okhttp-interceptor-no-op/proguard-rules.pro diff --git a/FloconAndroid/okhttp-interceptor-no-op/src/main/AndroidManifest.xml b/FloconAndroid/network/okhttp-interceptor-no-op/src/main/AndroidManifest.xml similarity index 100% rename from FloconAndroid/okhttp-interceptor-no-op/src/main/AndroidManifest.xml rename to FloconAndroid/network/okhttp-interceptor-no-op/src/main/AndroidManifest.xml diff --git a/FloconAndroid/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt b/FloconAndroid/network/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt similarity index 100% rename from FloconAndroid/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt rename to FloconAndroid/network/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt diff --git a/FloconAndroid/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt b/FloconAndroid/network/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt similarity index 100% rename from FloconAndroid/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt rename to FloconAndroid/network/okhttp-interceptor-no-op/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt diff --git a/FloconAndroid/okhttp-interceptor/.gitignore b/FloconAndroid/network/okhttp-interceptor/.gitignore similarity index 100% rename from FloconAndroid/okhttp-interceptor/.gitignore rename to FloconAndroid/network/okhttp-interceptor/.gitignore diff --git a/FloconAndroid/okhttp-interceptor/build.gradle.kts b/FloconAndroid/network/okhttp-interceptor/build.gradle.kts similarity index 98% rename from FloconAndroid/okhttp-interceptor/build.gradle.kts rename to FloconAndroid/network/okhttp-interceptor/build.gradle.kts index 5388747f0..2ab89d589 100644 --- a/FloconAndroid/okhttp-interceptor/build.gradle.kts +++ b/FloconAndroid/network/okhttp-interceptor/build.gradle.kts @@ -41,7 +41,7 @@ kotlin { dependencies { - implementation(project(":flocon")) + implementation(project(":network:core")) implementation(platform(libs.kotlinx.coroutines.bom)) implementation(libs.jetbrains.kotlinx.coroutines.core) diff --git a/FloconAndroid/okhttp-interceptor/consumer-rules.pro b/FloconAndroid/network/okhttp-interceptor/consumer-rules.pro similarity index 100% rename from FloconAndroid/okhttp-interceptor/consumer-rules.pro rename to FloconAndroid/network/okhttp-interceptor/consumer-rules.pro diff --git a/FloconAndroid/okhttp-interceptor/proguard-rules.pro b/FloconAndroid/network/okhttp-interceptor/proguard-rules.pro similarity index 100% rename from FloconAndroid/okhttp-interceptor/proguard-rules.pro rename to FloconAndroid/network/okhttp-interceptor/proguard-rules.pro diff --git a/FloconAndroid/okhttp-interceptor/src/main/AndroidManifest.xml b/FloconAndroid/network/okhttp-interceptor/src/main/AndroidManifest.xml similarity index 100% rename from FloconAndroid/okhttp-interceptor/src/main/AndroidManifest.xml rename to FloconAndroid/network/okhttp-interceptor/src/main/AndroidManifest.xml diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt similarity index 100% rename from FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt rename to FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt similarity index 100% rename from FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt rename to FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt similarity index 100% rename from FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt rename to FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt similarity index 100% rename from FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt rename to FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt similarity index 100% rename from FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt rename to FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt diff --git a/FloconAndroid/okhttp-interceptor/src/test/kotlin/io/github/openflocon/flocon/okhttp/UtilsTest.kt b/FloconAndroid/network/okhttp-interceptor/src/test/kotlin/io/github/openflocon/flocon/okhttp/UtilsTest.kt similarity index 100% rename from FloconAndroid/okhttp-interceptor/src/test/kotlin/io/github/openflocon/flocon/okhttp/UtilsTest.kt rename to FloconAndroid/network/okhttp-interceptor/src/test/kotlin/io/github/openflocon/flocon/okhttp/UtilsTest.kt diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index c8dc04fa9..64ecd4299 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -87,11 +87,11 @@ dependencies { debugImplementation(project(":deeplinks")) releaseImplementation(project(":deeplinks-no-op")) - debugImplementation(project(":okhttp-interceptor")) - releaseImplementation(project(":okhttp-interceptor-no-op")) + debugImplementation(project(":network:okhttp-interceptor")) + releaseImplementation(project(":network:okhttp-interceptor-no-op")) implementation(project(":grpc:grpc-interceptor-lite")) - debugImplementation(project(":ktor-interceptor")) - releaseImplementation(project(":ktor-interceptor-no-op")) + debugImplementation(project(":network:ktor-interceptor")) + releaseImplementation(project(":network:ktor-interceptor-no-op")) debugImplementation(project(":datastores")) releaseImplementation(project(":datastores-no-op")) } diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index 1cad46fc0..51ba4847a 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -34,7 +34,7 @@ kotlin { val commonMain by getting { dependencies { implementation(project(":flocon")) - implementation(project(":ktor-interceptor")) + implementation(project(":network:ktor-interceptor")) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index 4b3e4db49..1009160d5 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -20,14 +20,16 @@ include(":sample-android-only") include(":sample-multiplatform") include(":flocon") include(":flocon-no-op") -include(":okhttp-interceptor") -include(":okhttp-interceptor-no-op") +include(":network:okhttp-interceptor") +include(":network:okhttp-interceptor-no-op") include(":grpc:grpc-interceptor") include(":grpc:grpc-interceptor-base") include(":grpc:grpc-interceptor-lite") -include(":ktor-interceptor") -include(":ktor-interceptor-no-op") +include(":network:ktor-interceptor") +include(":network:ktor-interceptor-no-op") include(":datastores") include(":datastores-no-op") include(":deeplinks") include(":deeplinks-no-op") +include(":network:core") +include(":network:core-no-op") From f490aca536d096aaab2a93475c81ee55eb54f91e Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 14:04:33 +0100 Subject: [PATCH 08/38] feat: Move plugin --- .../FloconNetworkPluginImpl.android.kt | 7 -- .../network/FloconNetworkPluginImpl.jvm.kt | 77 ---------------- .../core}/FloconNetworkPluginImpl.android.kt | 28 +++--- .../network/core}/FloconNetworkPluginImpl.kt | 14 ++- .../network/core}/mapper/BadQualityToJson.kt | 2 +- .../mapper/FloconNetworkRequestToJson.kt | 2 +- .../core}/mapper/MockResponseToJson.kt | 2 +- .../flocon/network/core}/mapper/Websocket.kt | 2 +- .../core}/FloconNetworkPluginImpl.ios.kt | 6 +- .../core/FloconNetworkPluginImpl.jvm.kt | 91 +++++++++++++++++++ 10 files changed, 117 insertions(+), 114 deletions(-) delete mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.android.kt rename FloconAndroid/{flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/network => network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core}/FloconNetworkPluginImpl.android.kt (73%) rename FloconAndroid/{flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network => network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core}/FloconNetworkPluginImpl.kt (90%) rename FloconAndroid/{flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network => network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core}/mapper/BadQualityToJson.kt (98%) rename FloconAndroid/{flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network => network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core}/mapper/FloconNetworkRequestToJson.kt (98%) rename FloconAndroid/{flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network => network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core}/mapper/MockResponseToJson.kt (98%) rename FloconAndroid/{flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network => network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core}/mapper/Websocket.kt (92%) rename FloconAndroid/{flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/network => network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core}/FloconNetworkPluginImpl.ios.kt (75%) create mode 100644 FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.jvm.kt diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.android.kt deleted file mode 100644 index 9c6088f1e..000000000 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.android.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.openflocon.flocon.plugins.network - -import io.github.openflocon.flocon.FloconContext - -internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { - TODO("Not yet implemented") -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.jvm.kt index c6729c0f3..f113a8b4f 100644 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.jvm.kt +++ b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.jvm.kt @@ -12,80 +12,3 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { - return FloconNetworkDataSourceJvm() -} - -internal class FloconNetworkDataSourceJvm( -) : FloconNetworkDataSource { - - private val baseDir: File = File(System.getProperty("user.home"), ".flocon") - - init { - if (!baseDir.exists()) { - baseDir.mkdirs() - } - } - - override fun saveMocksToFile(mocks: List) { - try { - val file = File(baseDir, FLOCON_NETWORK_MOCKS_JSON) - val jsonString = writeMockResponsesToJson(mocks) - FileOutputStream(file).use { - it.write(jsonString.toByteArray(Charsets.UTF_8)) - } - } catch (t: Throwable) { - FloconLogger.logError("issue in saveMocksToFile", t) - } - } - - override fun loadMocksFromFile(): List { - return try { - val file = File(baseDir, FLOCON_NETWORK_MOCKS_JSON) - if (!file.exists()) { - return emptyList() - } - - val jsonString = FileInputStream(file).use { - it.readBytes().toString(Charsets.UTF_8) - } - parseMockResponses(jsonString) - } catch (t: Throwable) { - FloconLogger.logError("issue in loadMocksFromFile", t) - emptyList() - } - } - - override fun loadBadNetworkConfig(): BadQualityConfig? { - return try { - val file = File(baseDir, 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 - } - } - - override fun saveBadNetworkConfig(config: BadQualityConfig?) { - try { - val file = File(baseDir, FLOCON_NETWORK_BAD_CONFIG_JSON) - if (config == null) { - file.delete() - } else { - val jsonString = config.toJsonString() - FileOutputStream(file).use { - it.write(jsonString.toByteArray(Charsets.UTF_8)) - } - } - } catch (t: Throwable) { - FloconLogger.logError("issue in saveBadNetworkConfig", t) - } - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPluginImpl.android.kt b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.android.kt similarity index 73% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPluginImpl.android.kt rename to FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.android.kt index 0b7493ae0..e11512cc2 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPluginImpl.android.kt +++ b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.android.kt @@ -1,29 +1,27 @@ -package io.github.openflocon.flocon.pluginsold.network +package io.github.openflocon.flocon.network.core import android.content.Context import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.plugins.network.FLOCON_NETWORK_BAD_CONFIG_JSON -import io.github.openflocon.flocon.plugins.network.FLOCON_NETWORK_MOCKS_JSON -import io.github.openflocon.flocon.plugins.network.FloconNetworkDataSource -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.toJsonString -import io.github.openflocon.flocon.plugins.network.mapper.writeMockResponsesToJson +import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig +import io.github.openflocon.flocon.network.core.mapper.parseMockResponses +import io.github.openflocon.flocon.network.core.mapper.toJsonString +import io.github.openflocon.flocon.network.core.mapper.writeMockResponsesToJson import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -//internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { -// return FloconNetworkDataSourceAndroid( -// context = context.context, -// ) -//} +internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { + return FloconNetworkDataSourceAndroid( + context = context.context, + ) +} -internal class FloconNetworkDataSourceAndroid(private val context: Context) : - FloconNetworkDataSource { +internal class FloconNetworkDataSourceAndroid( + private val context: Context +) : FloconNetworkDataSource { override fun saveMocksToFile(mocks: List) { try { val file = File(context.filesDir, FLOCON_NETWORK_MOCKS_JSON) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt similarity index 90% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt rename to FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt index 6d2a9c0b8..f6d5a0a5d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network +package io.github.openflocon.flocon.network.core import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconContext @@ -7,13 +7,11 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender -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.floconNetworkWebSocketEventToJson -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.parseWebSocketMockMessage -import io.github.openflocon.flocon.plugins.network.mapper.webSocketIdsToJsonArray +import io.github.openflocon.flocon.network.core.mapper.floconNetworkCallResponseToJson +import io.github.openflocon.flocon.network.core.mapper.floconNetworkWebSocketEventToJson +import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig +import io.github.openflocon.flocon.network.core.mapper.parseMockResponses +import io.github.openflocon.flocon.network.core.mapper.parseWebSocketMockMessage import io.github.openflocon.flocon.pluginsold.network.FloconNetworkConfig import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/BadQualityToJson.kt similarity index 98% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt rename to FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/BadQualityToJson.kt index 97112eb6f..0b4c0950e 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/BadQualityToJson.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/BadQualityToJson.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.mapper +package io.github.openflocon.flocon.network.core.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt similarity index 98% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt rename to FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt index e8b849d6c..45d3977cd 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalUuidApi::class) -package io.github.openflocon.flocon.plugins.network.mapper +package io.github.openflocon.flocon.network.core.mapper import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt similarity index 98% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt rename to FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt index fd6efd088..ba7227eed 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.mapper +package io.github.openflocon.flocon.network.core.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/Websocket.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt similarity index 92% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/Websocket.kt rename to FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt index 5212883e4..8eeaeb931 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/network/mapper/Websocket.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.network.mapper +package io.github.openflocon.flocon.network.core.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.ios.kt b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.ios.kt similarity index 75% rename from FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.ios.kt rename to FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.ios.kt index bdccd4100..dc587f51f 100644 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.ios.kt +++ b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.ios.kt @@ -1,8 +1,8 @@ -package io.github.openflocon.flocon.plugins.network +package io.github.openflocon.flocon.network.core import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig -import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { return FloconNetworkDataSourceIOs() diff --git a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.jvm.kt b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.jvm.kt new file mode 100644 index 000000000..16342afe8 --- /dev/null +++ b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.jvm.kt @@ -0,0 +1,91 @@ +package io.github.openflocon.flocon.network.core + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig +import io.github.openflocon.flocon.network.core.mapper.parseMockResponses +import io.github.openflocon.flocon.network.core.mapper.toJsonString +import io.github.openflocon.flocon.network.core.mapper.writeMockResponsesToJson +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + +internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { + return FloconNetworkDataSourceJvm() +} + +internal class FloconNetworkDataSourceJvm( +) : FloconNetworkDataSource { + + private val baseDir: File = File(System.getProperty("user.home"), ".flocon") + + init { + if (!baseDir.exists()) { + baseDir.mkdirs() + } + } + + override fun saveMocksToFile(mocks: List) { + try { + val file = File(baseDir, FLOCON_NETWORK_MOCKS_JSON) + val jsonString = writeMockResponsesToJson(mocks) + FileOutputStream(file).use { + it.write(jsonString.toByteArray(Charsets.UTF_8)) + } + } catch (t: Throwable) { + FloconLogger.logError("issue in saveMocksToFile", t) + } + } + + override fun loadMocksFromFile(): List { + return try { + val file = File(baseDir, FLOCON_NETWORK_MOCKS_JSON) + if (!file.exists()) { + return emptyList() + } + + val jsonString = FileInputStream(file).use { + it.readBytes().toString(Charsets.UTF_8) + } + parseMockResponses(jsonString) + } catch (t: Throwable) { + FloconLogger.logError("issue in loadMocksFromFile", t) + emptyList() + } + } + + override fun loadBadNetworkConfig(): BadQualityConfig? { + return try { + val file = File(baseDir, 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 + } + } + + override fun saveBadNetworkConfig(config: BadQualityConfig?) { + try { + val file = File(baseDir, FLOCON_NETWORK_BAD_CONFIG_JSON) + if (config == null) { + file.delete() + } else { + val jsonString = config.toJsonString() + FileOutputStream(file).use { + it.write(jsonString.toByteArray(Charsets.UTF_8)) + } + } + } catch (t: Throwable) { + FloconLogger.logError("issue in saveBadNetworkConfig", t) + } + } +} \ No newline at end of file From 754251f7402bce22107011a7cff2317a3a34159f Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 15:07:36 +0100 Subject: [PATCH 09/38] feat: OkHttp work --- .../plugins/deeplinks/FloconDeeplinks.kt | 5 - .../io/github/openflocon/flocon/FloconApp.kt | 3 +- .../openflocon/flocon/FloconConfiguration.kt | 38 +++- .../io/github/openflocon/flocon/FloconCore.kt | 2 +- .../github/openflocon/flocon/FloconPlugin.kt | 2 +- .../flocon/core/FloconMessageSender.kt | 2 + .../openflocon/flocon/dsl/FloconMarker.kt | 14 ++ .../flocon/error/PluginNotInitialized.kt | 6 + .../analytics/FloconAnalyticsPlugin.kt | 1 + .../database/FloconDatabasePlugin.kt | 2 + .../pluginsold/device/FloconDevicePlugin.kt | 2 + .../pluginsold/files/FloconFilesPlugin.kt | 2 + .../pluginsold/network/FloconNetworkPlugin.kt | 18 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 2 + .../pluginsold/tables/FloconTablesPlugin.kt | 2 + .../network/FloconNetworkPluginImpl.jvm.kt | 14 -- .../network/core/FloconNetworkPluginImpl.kt | 51 +++-- .../network/ktor-interceptor/build.gradle.kts | 5 +- .../okhttp-interceptor/build.gradle.kts | 2 +- .../flocon/okhttp/OkHttpInterceptor.kt | 210 +++++++++--------- .../flocon/myapplication/MainActivity.kt | 41 ++-- 21 files changed, 239 insertions(+), 185 deletions(-) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/dsl/FloconMarker.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/error/PluginNotInitialized.kt delete mode 100644 FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.jvm.kt diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index 2e9e69375..c5a3c8aa8 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -34,26 +34,21 @@ internal class FloconDeeplinksPluginImpl( method: String, body: String, ) { - println("Deeplinks: message received $($method) ($body)") // no op } override suspend fun onConnectedToServer() { - println("Deeplinks: connected (${deeplinks})") registerDeeplinks(deeplinks) } override suspend fun registerDeeplinks(deeplinks: List) { try { - println("Deeplinks: sending") sender.send( plugin = Protocol.FromDevice.Deeplink.Plugin, method = Protocol.FromDevice.Deeplink.Method.GetDeeplinks, body = toDeeplinksJson(deeplinks) ) - println("Deeplinks: sent") } catch (t: Throwable) { - println("Deeplinks: error: ${t.message}") t.printStackTrace() FloconLogger.logError("deeplink mapping error", t) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt index 176c16416..7b2eafcf8 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt @@ -7,7 +7,8 @@ abstract class FloconApp { companion object { var instance: FloconApp? = null - private set + get() = field ?: error("FloconApp is not initialized") + internal set } interface Client { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index e1253bf15..11dd42161 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -10,10 +10,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class FloconConfiguration internal constructor( + private val context: FloconContext, private val client: FloconClient ) { - internal val plugins = mutableListOf() + private val plugins: MutableMap FloconPlugin> = mutableMapOf() /** * Install a plugin with the given [factory] and optional [configure] block. @@ -22,13 +23,24 @@ class FloconConfiguration internal constructor( factory: FloconPluginFactory, configure: Config.() -> Unit = {} ) { - val plugin = factory.install( - config = factory.createConfig() - .apply { configure() }, - app = DumpObject(client = client) + plugins[factory.pluginId] = { scope -> + val config = factory.createConfig() + .apply { configure() } + + factory.install( + config = config, + app = scope + ) + } + } + + fun build(): List { + val app = DumpObject( + context = context, + client = client ) - plugins.add(plugin) + return plugins.values.map { it.invoke(app) } } } @@ -36,17 +48,22 @@ class FloconConfiguration internal constructor( fun startFlocon(context: FloconContext, block: FloconConfiguration.() -> Unit) { val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) val client = FloconClient(context = context) - val configuration = FloconConfiguration(client = client).apply(block) + val configuration = FloconConfiguration( + context = context, + client = client + ) + .apply(block) Flocon( context = context, scope = scope, client = client, - plugins = configuration.plugins + plugins = configuration.build() ) } class DumpObject( + context: FloconContext, client: Client ) : FloconApp() { @@ -55,4 +72,9 @@ class DumpObject( private val _initialized = MutableStateFlow(false) override val isInitialized: StateFlow = _initialized.asStateFlow() + init { + this.context = context + instance = this + } + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt index be8a0c673..996721541 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt @@ -29,7 +29,7 @@ abstract class FloconCore : FloconApp() { protected fun initializeFlocon( context: FloconContext, - configuration: FloconConfiguration = FloconConfiguration(TODO()) + configuration: FloconConfiguration = FloconConfiguration(TODO(), TODO()) ) { //super.initializeFlocon(context) // val newClient = FloconClientImpl(context, configuration) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt index 53865e802..65aa4bbe0 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -22,7 +22,7 @@ interface FloconPluginConfig */ interface FloconPluginKey { val name: String - val pluginId: String? get() = null + val pluginId: String } /** diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt index dbff6b15b..a61ba4772 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.core interface FloconMessageSender { + suspend fun send( plugin: String, method: String, @@ -8,4 +9,5 @@ interface FloconMessageSender { ) suspend fun sendPendingMessages() + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/dsl/FloconMarker.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/dsl/FloconMarker.kt new file mode 100644 index 000000000..80868b751 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/dsl/FloconMarker.kt @@ -0,0 +1,14 @@ +package io.github.openflocon.flocon.dsl + +@RequiresOptIn( + message = "Used to mark internal Flocon APIs", + level = RequiresOptIn.Level.ERROR +) +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.FIELD, + AnnotationTarget.PROPERTY +) +annotation class FloconMarker diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/error/PluginNotInitialized.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/error/PluginNotInitialized.kt new file mode 100644 index 000000000..60da21935 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/error/PluginNotInitialized.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.error + +import io.github.openflocon.flocon.dsl.FloconMarker + +@FloconMarker +fun pluginNotInitialized(pluginName: String): Nothing = error("$pluginName is not initialized") \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt index a64cc3866..44ad622b9 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt @@ -16,6 +16,7 @@ object FloconAnalytics : FloconPluginFactory { override val name: String get() = TODO("Not yet implemented") + override val pluginId: String + get() = TODO("Not yet implemented") } interface FloconFilesPlugin : FloconPlugin \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt index 2f220e3b2..8836baf08 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt @@ -1,9 +1,7 @@ package io.github.openflocon.flocon.pluginsold.network -import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse @@ -16,18 +14,6 @@ class FloconNetworkConfig : FloconPluginConfig { val mocks = mutableListOf() } -/** - * Flocon Network Plugin. - * Used to inspect HTTP/S and WebSocket calls. - */ -object FloconNetwork : FloconPluginFactory { - override fun createConfig(): FloconNetworkConfig = TODO() - override fun install(config: FloconNetworkConfig, app: FloconApp): FloconNetworkPlugin = TODO() - - override val name: String = "" -} - - interface FloconNetworkPlugin : FloconPlugin { val mocks: Collection val badQualityConfig: BadQualityConfig? @@ -35,9 +21,9 @@ interface FloconNetworkPlugin : FloconPlugin { fun logRequest(request: FloconNetworkCallRequest) fun logResponse(response: FloconNetworkCallResponse) - fun logWebSocket( + suspend fun logWebSocket( event: FloconWebSocketEvent, ) - fun registerWebSocketMockListener(id: String, listener: FloconWebSocketMockListener) + suspend fun registerWebSocketMockListener(id: String, listener: FloconWebSocketMockListener) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt index 6beb16aa2..9af248027 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt @@ -23,6 +23,8 @@ object FloconPreferences : FloconPluginFactory { override val name: String get() = TODO("Not yet implemented") + override val pluginId: String + get() = TODO("Not yet implemented") } //fun floconTable(tableName: String) : TableBuilder { diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.jvm.kt deleted file mode 100644 index f113a8b4f..000000000 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.jvm.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.openflocon.flocon.plugins.network - -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -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.toJsonString -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.MockNetworkResponse -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream - diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt index f6d5a0a5d..e0057fc06 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt @@ -7,11 +7,15 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.error.pluginNotInitialized +import io.github.openflocon.flocon.network.core.mapper.floconNetworkCallRequestToJson import io.github.openflocon.flocon.network.core.mapper.floconNetworkCallResponseToJson import io.github.openflocon.flocon.network.core.mapper.floconNetworkWebSocketEventToJson import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig import io.github.openflocon.flocon.network.core.mapper.parseMockResponses import io.github.openflocon.flocon.network.core.mapper.parseWebSocketMockMessage +import io.github.openflocon.flocon.network.core.mapper.webSocketIdsToJsonArray import io.github.openflocon.flocon.pluginsold.network.FloconNetworkConfig import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig @@ -39,6 +43,7 @@ object FloconNetwork : FloconPluginFactory>(emptyMap()) - private val _mocks = MutableStateFlow>(dataSource.loadMocksFromFile()) + private val _mocks = MutableStateFlow(dataSource.loadMocksFromFile()) override val mocks: List get() = _mocks.value - private val _badQualityConfig = - MutableStateFlow(dataSource.loadBadNetworkConfig()) + private val _badQualityConfig = MutableStateFlow(dataSource.loadBadNetworkConfig()) override val badQualityConfig: BadQualityConfig? get() = _badQualityConfig.value override fun logRequest(request: FloconNetworkCallRequest) { try { -// sender.send( -// plugin = Protocol.FromDevice.Network.Plugin, -// method = Protocol.FromDevice.Network.Method.LogNetworkCallRequest, -// body = request.floconNetworkCallRequestToJson(), -// ) + coroutineScope.launch { + sender.send( + plugin = Protocol.FromDevice.Network.Plugin, + method = Protocol.FromDevice.Network.Method.LogNetworkCallRequest, + body = request.floconNetworkCallRequestToJson(), + ) + } } catch (t: Throwable) { FloconLogger.logError("Network json mapping error", t) } @@ -103,7 +114,7 @@ internal class FloconNetworkPluginImpl( } } - override fun logWebSocket( + override suspend fun logWebSocket( event: FloconWebSocketEvent, ) { coroutineScope.launch { @@ -149,7 +160,7 @@ internal class FloconNetworkPluginImpl( updateWebSocketIds() } - override fun registerWebSocketMockListener( + override suspend fun registerWebSocketMockListener( id: String, listener: FloconWebSocketMockListener ) { @@ -159,11 +170,15 @@ internal class FloconNetworkPluginImpl( updateWebSocketIds() } - private fun updateWebSocketIds() { -// sender.send( -// plugin = Protocol.FromDevice.Network.Plugin, -// method = Protocol.FromDevice.Network.Method.RegisterWebSocketIds, -// body = webSocketIdsToJsonArray(ids = websocketListeners.value.keys), -// ) + private suspend fun updateWebSocketIds() { + sender.send( + plugin = Protocol.FromDevice.Network.Plugin, + method = Protocol.FromDevice.Network.Method.RegisterWebSocketIds, + body = webSocketIdsToJsonArray(ids = websocketListeners.value.keys), + ) + } + + companion object { + var plugin: FloconNetworkPlugin? = null } } \ No newline at end of file diff --git a/FloconAndroid/network/ktor-interceptor/build.gradle.kts b/FloconAndroid/network/ktor-interceptor/build.gradle.kts index 0bc55cdc3..2404e2b0b 100644 --- a/FloconAndroid/network/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor/build.gradle.kts @@ -22,7 +22,10 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":network:core")) + + api(project(":flocon")) + api(project(":network:core")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") implementation(libs.ktor.client.core) } diff --git a/FloconAndroid/network/okhttp-interceptor/build.gradle.kts b/FloconAndroid/network/okhttp-interceptor/build.gradle.kts index 2ab89d589..a30d90427 100644 --- a/FloconAndroid/network/okhttp-interceptor/build.gradle.kts +++ b/FloconAndroid/network/okhttp-interceptor/build.gradle.kts @@ -41,7 +41,7 @@ kotlin { dependencies { - implementation(project(":network:core")) + api(project(":network:core")) implementation(platform(libs.kotlinx.coroutines.bom)) implementation(libs.jetbrains.kotlinx.coroutines.core) diff --git a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt index 88e0cf095..1986f8f42 100644 --- a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt +++ b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt @@ -2,8 +2,14 @@ package io.github.openflocon.flocon.okhttp +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.network.core.networkPlugin +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkResponse import okhttp3.Interceptor +import okhttp3.MediaType import okhttp3.Request import okhttp3.Response import java.io.IOException @@ -23,14 +29,15 @@ class FloconOkhttpInterceptor( @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { - val floconNetworkPlugin = TODO()//FloconApp.instance?.client?.networkPlugin - if (floconNetworkPlugin == null || !shouldLog(chain)) { + val floconNetworkPlugin = FloconApp.instance!!.networkPlugin + + if (!shouldLog(chain)) { // on no op, do not intercept the call, just execute it return chain.proceed(chain.request()) } - Uuid.random().toString() - "http" + val floconCallId = Uuid.random().toString() + val floconNetworkType = "http" val request = chain.request() @@ -60,7 +67,7 @@ class FloconOkhttpInterceptor( ) val isMocked = mockConfig != null - FloconNetworkRequest( + val floconNetworkRequest = FloconNetworkRequest( url = request.url.toString(), method = request.method, startTime = requestedAt, @@ -70,119 +77,118 @@ class FloconOkhttpInterceptor( isMocked = isMocked, ) -// floconNetworkPlugin.logRequest( -// FloconNetworkCallRequest( -// floconCallId = floconCallId, -// floconNetworkType = floconNetworkType, -// isMocked = isMocked, -// request = floconNetworkRequest, -// ) -// ) + floconNetworkPlugin.logRequest( + FloconNetworkCallRequest( + floconCallId = floconCallId, + floconNetworkType = floconNetworkType, + isMocked = isMocked, + request = floconNetworkRequest, + ) + ) try { - val response = TODO() -// if (isMocked) { -// executeMock(request = request, mock = mockConfig, requestHeaders = requestHeadersMap) -// } else { -// floconNetworkPlugin.badQualityConfig?.let { badQualityConfig -> -// executeBadQuality( -// badQualityConfig = badQualityConfig, -// request = request, -// ) -// } ?: run { -// chain.proceed(request) -// } -// } + val response = if (isMocked) { + executeMock( + request = request, + mock = mockConfig, + requestHeaders = requestHeadersMap + ) + } else { + floconNetworkPlugin.badQualityConfig?.let { badQualityConfig -> + executeBadQuality( + badQualityConfig = badQualityConfig, + request = request, + ) + } ?: run { + chain.proceed(request) + } + } val endTime = System.nanoTime() - - (endTime - startTime) / 1e6 + val durationMs: Double = (endTime - startTime) / 1e6 // To get the response body, be careful // because the body can only be read once. // It must be duplicated so that the chain can continue normally. -// val responseBody = response.body -// var responseBodyString: String? = null -// var responseSize: Long? = null -// val responseContentType: MediaType? = responseBody?.contentType() -// -// val responseHeadersMap = -// response.headers.toMultimap().mapValues { it.value.joinToString(",") } -// -// if (responseBody != null) { -// val (bodyString, bodySize) = extractResponseBodyInfo( -// response = response, -// responseHeaders = responseHeadersMap, -// ) -// responseBodyString = bodyString -// responseSize = bodySize -// } - -// val isImage = -// responseContentType?.toString()?.startsWith("image/") == true || (isImage?.invoke( -// FloconNetworkIsImageParams( -// request = request, -// response = response, -// responseContentType = responseContentType?.toString(), -// ) -// ) == true) -// -// val requestHeadersMapUpToDate = -// response.request.headers.toMultimap().mapValues { it.value.joinToString(",") } -// -// val floconCallResponse = FloconNetworkResponse( -// httpCode = response.code, -// contentType = responseContentType?.toString(), -// body = responseBodyString.takeUnless { isImage }, // dont send images responses bytes -// headers = responseHeadersMap, -// size = responseSize, -// grpcStatus = null, -// error = null, -// requestHeaders = requestHeadersMapUpToDate, -// isImage = isImage, -// ) -// -// floconNetworkPlugin.logResponse( -// FloconNetworkCallResponse( -// floconCallId = floconCallId, -// durationMs = durationMs, -// floconNetworkType = floconNetworkType, -// isMocked = isMocked, -// response = floconCallResponse, -// ) -// ) + val responseBody = response.body + var responseBodyString: String? = null + var responseSize: Long? = null + val responseContentType: MediaType? = responseBody?.contentType() + + val responseHeadersMap = + response.headers.toMultimap().mapValues { it.value.joinToString(",") } + + if (responseBody != null) { + val (bodyString, bodySize) = extractResponseBodyInfo( + response = response, + responseHeaders = responseHeadersMap, + ) + responseBodyString = bodyString + responseSize = bodySize + } + + val isImage = + responseContentType?.toString()?.startsWith("image/") == true || (isImage?.invoke( + FloconNetworkIsImageParams( + request = request, + response = response, + responseContentType = responseContentType?.toString(), + ) + ) == true) + + val requestHeadersMapUpToDate = + response.request.headers.toMultimap().mapValues { it.value.joinToString(",") } + + val floconCallResponse = FloconNetworkResponse( + httpCode = response.code, + contentType = responseContentType?.toString(), + body = responseBodyString.takeUnless { isImage }, // dont send images responses bytes + headers = responseHeadersMap, + size = responseSize, + grpcStatus = null, + error = null, + requestHeaders = requestHeadersMapUpToDate, + isImage = isImage, + ) + + floconNetworkPlugin.logResponse( + FloconNetworkCallResponse( + floconCallId = floconCallId, + durationMs = durationMs, + floconNetworkType = floconNetworkType, + isMocked = isMocked, + response = floconCallResponse, + ) + ) // Rebuild the response with a new body so that the chain can continue // The original response body is already consumed by peekBody, so no need to rebuild with it. // Just return the original response if you don't modify the body itself. return response } catch (e: IOException) { - val endTime = System.nanoTime() + val durationMs: Double = (endTime - startTime) / 1e6 + val floconCallResponse = FloconNetworkResponse( + httpCode = null, + contentType = null, + body = null, + headers = emptyMap(), + size = null, + grpcStatus = null, + error = e.message ?: e.javaClass.simpleName, + requestHeaders = null, + isImage = false, + ) - (endTime - startTime) / 1e6 - -// val floconCallResponse = FloconNetworkResponse( -// httpCode = null, -// contentType = null, -// body = null, -// headers = emptyMap(), -// size = null, -// grpcStatus = null, -// error = e.message ?: e.javaClass.simpleName, -// requestHeaders = null, -// isImage = false, -// ) - -// floconNetworkPlugin.logResponse( -// FloconNetworkCallResponse( -// floconCallId = floconCallId, -// durationMs = durationMs, -// floconNetworkType = floconNetworkType, -// isMocked = isMocked, -// response = floconCallResponse, -// ) -// ) + floconNetworkPlugin.logResponse( + FloconNetworkCallResponse( + floconCallId = floconCallId, + durationMs = durationMs, + floconNetworkType = floconNetworkType, + isMocked = isMocked, + response = floconCallResponse, + ) + ) throw e } } diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index 3adc8f327..99ef90936 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -27,10 +27,13 @@ import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.grpc.GrpcController import io.github.openflocon.flocon.myapplication.ui.ImagesListView import io.github.openflocon.flocon.myapplication.ui.theme.MyApplicationTheme +import io.github.openflocon.flocon.network.core.FloconNetwork +import io.github.openflocon.flocon.okhttp.FloconOkhttpInterceptor import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks import io.github.openflocon.flocon.startFlocon import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import okhttp3.OkHttpClient import kotlin.random.Random import kotlin.uuid.ExperimentalUuidApi @@ -46,21 +49,21 @@ class MainActivity : ComponentActivity() { Toast.makeText(this, "opend with : $it", Toast.LENGTH_LONG).show() } -// val okHttpClient = OkHttpClient() -// .newBuilder() -// .addInterceptor( -// FloconOkhttpInterceptor( -// isImage = { -// it.request.url.toString().contains("picsum") -// }, -// /*shouldLog = { -// val url = it.request().url.toString() -// println("url: $url") -// url.contains("1").not() -// }*/ -// ) -// ) -// .build() + val okHttpClient = OkHttpClient() + .newBuilder() + .addInterceptor( + FloconOkhttpInterceptor( + isImage = { + it.request.url.toString().contains("picsum") + }, + /*shouldLog = { + val url = it.request().url.toString() + println("url: $url") + url.contains("1").not() + }*/ + ) + ) + .build() // initializeSharedPreferences(applicationContext) // initializeDatabases(context = applicationContext) @@ -72,7 +75,7 @@ class MainActivity : ComponentActivity() { // initializeSharedPreferencesAfterInit(applicationContext) // initializeDatastores(applicationContext) -// val dummyHttpCaller = DummyHttpCaller(client = okHttpClient) + val dummyHttpCaller = DummyHttpCaller(client = okHttpClient) // val dummyWebsocketCaller = DummyWebsocketCaller(client = okHttpClient) // GlobalScope.launch { dummyWebsocketCaller.connectToWebsocket() } // val graphQlTester = GraphQlTester(client = okHttpClient) @@ -102,7 +105,7 @@ class MainActivity : ComponentActivity() { ) { Button( onClick = { - //dummyHttpCaller.call() + dummyHttpCaller.call() } ) { Text("okhttp test") @@ -228,6 +231,10 @@ class MainActivity : ComponentActivity() { description = "Open a post and send a comment" ) } + + install(FloconNetwork) { + + } } } From a523461ea1048a33c5b8cae7a1e677d79ee9c408 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 16:33:02 +0100 Subject: [PATCH 10/38] feat: Clean install --- .../plugins/deeplinks/FloconDeeplinks.kt | 16 ++- .../files/FloconFilesPlugin.android.kt | 5 + .../websocket/FloconHttpClient.android.kt | 7 +- .../websocket/FloconHttpClientAndroid.kt | 2 + .../io/github/openflocon/flocon/Flocon.kt | 27 +++-- .../openflocon/flocon/FloconConfiguration.kt | 36 +++--- .../io/github/openflocon/flocon/FloconCore.kt | 84 ------------- .../io/github/openflocon/flocon/FloconFile.kt | 5 +- .../github/openflocon/flocon/FloconPlugin.kt | 10 +- .../flocon/client/FloconClientImpl.kt | 110 +----------------- .../flocon/core/FloconFileSender.kt | 4 + .../openflocon/flocon/model/FloconFileInfo.kt | 5 +- .../analytics/FloconAnalyticsPlugin.kt | 10 +- .../FloconCrashReporterPlugin.kt | 8 +- .../dashboard/FloconDashboardPlugin.kt | 7 +- .../plugins/database/FloconDatabasePlugin.kt | 13 +-- .../plugins/device/FloconDevicePluginImpl.kt | 10 +- .../flocon/plugins/files/FloconFilesPlugin.kt | 19 ++- .../sharedprefs/FloconSharedPrefsPlugin.kt | 9 +- .../plugins/tables/FloconTablesPlugin.kt | 8 +- .../analytics/FloconAnalyticsPlugin.kt | 6 +- .../database/FloconDatabasePlugin.kt | 6 +- .../pluginsold/device/FloconDevicePlugin.kt | 5 +- .../pluginsold/files/FloconFilesPlugin.kt | 6 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 6 +- .../pluginsold/tables/FloconTablesPlugin.kt | 6 +- .../flocon/websocket/FloconHttpClient.kt | 7 +- .../io/github/openflocon/flocon/Flocon.ios.kt | 8 -- .../io/github/openflocon/flocon/Flocon.jvm.kt | 11 -- .../FloconNetworkDataSource.android.kt | 7 ++ .../FloconNetworkDataSourceAndroid.kt} | 11 +- .../flocon/network/core/FloconNetwork.kt | 41 +++++++ .../flocon/network/core/NetworkCore.kt | 3 - .../datasource/FloconNetworkDataSource.kt | 16 +++ .../{ => plugin}/FloconNetworkPluginImpl.kt | 39 +------ .../datasource/FloconNetworkDataSource.ios.kt | 7 ++ .../FloconNetworkDataSourceImpl.kt} | 9 +- .../datasource/FloconNetworkDataSource.jvm.kt | 7 ++ .../FloconNetworkDataSourceImpl.kt} | 11 +- .../flocon/okhttp/OkHttpInterceptor.kt | 4 +- .../repository/DeeplinkRepositoryImpl.kt | 2 - 41 files changed, 255 insertions(+), 358 deletions(-) delete mode 100644 FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/Flocon.ios.kt delete mode 100644 FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/Flocon.jvm.kt create mode 100644 FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.android.kt rename FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/{FloconNetworkPluginImpl.android.kt => datasource/FloconNetworkDataSourceAndroid.kt} (90%) create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt delete mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/NetworkCore.kt create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.kt rename FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/{ => plugin}/FloconNetworkPluginImpl.kt (78%) create mode 100644 FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.ios.kt rename FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/{FloconNetworkPluginImpl.ios.kt => datasource/FloconNetworkDataSourceImpl.kt} (62%) create mode 100644 FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.jvm.kt rename FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/{FloconNetworkPluginImpl.jvm.kt => datasource/FloconNetworkDataSourceImpl.kt} (91%) diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index c5a3c8aa8..3bae66feb 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -1,23 +1,27 @@ package io.github.openflocon.flocon.plugins.deeplinks -import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update object FloconDeeplinks : FloconPluginFactory { override val name: String = "Deeplinks" override val pluginId: String = FloconDeeplinks::class.simpleName!! override fun createConfig() = FloconDeeplinksConfig() - override fun install(config: FloconDeeplinksConfig, app: FloconApp): FloconDeeplinksPlugin { + + @OptIn(FloconMarker::class) + override fun install( + pluginConfig: FloconDeeplinksConfig, + floconConfig: FloconConfig + ): FloconDeeplinksPlugin { val plugin = FloconDeeplinksPluginImpl( - deeplinks = config.deeplinks, - sender = app.client as FloconMessageSender + deeplinks = pluginConfig.deeplinks, + sender = floconConfig.client as FloconMessageSender ) return plugin diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt index 5d8ad7a43..435aa072d 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt @@ -3,6 +3,7 @@ package io.github.openflocon.flocon.pluginsold.files import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconFile import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.plugins.files.FileDataSource import io.github.openflocon.flocon.plugins.files.model.fromdevice.FileDataModel import java.io.File @@ -10,6 +11,8 @@ import java.io.File internal class FileDataSourceAndroid( private val context: FloconContext, ) : FileDataSource { + + @FloconMarker override fun getFile( path: String, isConstantPath: Boolean @@ -26,6 +29,7 @@ internal class FileDataSourceAndroid( return file.takeIf { it.exists() }?.let { FloconFile(it) } } + @FloconMarker override fun getFolderContent( path: String, isConstantPath: Boolean, @@ -86,6 +90,7 @@ internal class FileDataSourceAndroid( } } + @FloconMarker override fun deleteFolderContent(folder: FloconFile) { deleteFolderContent(folder.file) } diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.android.kt index 6fa9bf0f0..92b85cda4 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.android.kt @@ -1,5 +1,6 @@ package io.github.openflocon.flocon.websocket -internal actual fun buildFloconHttpClient(): FloconHttpClient { - return FloconHttpClientAndroid() -} \ No newline at end of file +import io.github.openflocon.flocon.dsl.FloconMarker + +@FloconMarker +internal actual fun buildFloconHttpClient(): FloconHttpClient = FloconHttpClientAndroid() \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClientAndroid.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClientAndroid.kt index be744b5bf..28e5bda02 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClientAndroid.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClientAndroid.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.websocket import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.model.FloconFileInfo import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody @@ -12,6 +13,7 @@ import okhttp3.RequestBody.Companion.asRequestBody * The android client uses okhttp, because for android-only project there's more chance to having okhttp than ktor * it prevent conflicts with ktor versions also */ +@FloconMarker internal class FloconHttpClientAndroid : FloconHttpClient { private val client by lazy { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt index 31eab3f3d..075727480 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -1,28 +1,27 @@ +@file:OptIn(FloconMarker::class) + package io.github.openflocon.flocon import io.github.openflocon.flocon.FloconApp.Client -import io.github.openflocon.flocon.client.FloconClient import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.model.floconMessageFromServerFromJson -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -internal class Flocon( - private val context: FloconContext, - private val scope: CoroutineScope, - private val client: FloconClient, +class Flocon internal constructor( + private val config: FloconConfig, private val plugins: List ) { init { - scope.launch { + config.scope.launch { start( - client = client, - context = context, + client = config.client, + context = config.context, ) } } @@ -34,7 +33,7 @@ internal class Flocon( onClosed = { println("Client - Closed") // try again to connect - scope.launch { + config.scope.launch { start( client = client, context = context, @@ -68,7 +67,7 @@ internal class Flocon( } private fun onMessageReceived(message: String) { - scope.launch(Dispatchers.IO) { + config.scope.launch(Dispatchers.IO) { floconMessageFromServerFromJson(message)?.let { messageFromServer -> plugins.find { it.key == messageFromServer.plugin } ?.onMessageReceived( @@ -79,4 +78,10 @@ internal class Flocon( } } + companion object { + var instance: Flocon? = null + get() = field ?: error("Flocon is not initialized") + internal set + } + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index 11dd42161..fb8f00ce3 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon import io.github.openflocon.flocon.client.FloconClient +import io.github.openflocon.flocon.dsl.FloconMarker import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -10,11 +11,10 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class FloconConfiguration internal constructor( - private val context: FloconContext, - private val client: FloconClient + private val config: FloconConfig ) { - private val plugins: MutableMap FloconPlugin> = mutableMapOf() + private val plugins: MutableMap FloconPlugin> = mutableMapOf() /** * Install a plugin with the given [factory] and optional [configure] block. @@ -28,36 +28,36 @@ class FloconConfiguration internal constructor( .apply { configure() } factory.install( - config = config, - app = scope + pluginConfig = config, + floconConfig = scope ) } } fun build(): List { - val app = DumpObject( - context = context, - client = client - ) - - return plugins.values.map { it.invoke(app) } + return plugins.values.map { it.invoke(config) } } } +@ConsistentCopyVisibility +data class FloconConfig internal constructor( + val context: FloconContext, + val scope: CoroutineScope, + val client: FloconClient +) + fun startFlocon(context: FloconContext, block: FloconConfiguration.() -> Unit) { - val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - val client = FloconClient(context = context) - val configuration = FloconConfiguration( + val config = FloconConfig( context = context, - client = client + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + client = FloconClient(context = context) ) + val configuration = FloconConfiguration(config = config) .apply(block) Flocon( - context = context, - scope = scope, - client = client, + config = config, plugins = configuration.build() ) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt index 996721541..a834ef254 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt @@ -1,89 +1,5 @@ package io.github.openflocon.flocon -import io.github.openflocon.flocon.core.FloconMessageSender -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - expect class FloconContext -abstract class FloconCore : FloconApp() { - - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - private var _client: Client? = null - - override val client: Client? - get() { - return _client - } - - private val _isInitialized = MutableStateFlow(false) - override val isInitialized: StateFlow = _isInitialized - - protected fun initializeFlocon( - context: FloconContext, - configuration: FloconConfiguration = FloconConfiguration(TODO(), TODO()) - ) { - //super.initializeFlocon(context) - // val newClient = FloconClientImpl(context, configuration) - //_client = newClient - - // Setup crash handler early to catch crashes during initialization - //newClient.crashReporterPlugin?.setupCrashHandler() - - _isInitialized.value = true - -// scope.launch { -// start( -// client = newClient, -// context = context -// ) -// } - } - - private suspend fun start(client: Client, context: FloconContext) { - // try to connect, it fail : try again in 3s - try { - client.connect( - onClosed = { - // try again to connect - scope.launch { - start( - client = client, - context = context, - ) - } - }, - onMessageReceived = {} - ) - (client as? FloconMessageSender)?.let { - // if success, just send a bonjour - it.send("bonjour", method = "bonjour", body = "bonjour") - it.sendPendingMessages() - } - } catch (t: Throwable) { - if(t.message?.contains("CLEARTEXT communication to localhost not permitted by network security policy") == true) { - withContext(Dispatchers.Main) { - displayClearTextError(context = context) - } - } else { - //t.printStackTrace() - delay(3_000) - start( - client = client, - context = context, - ) - } - } - } - -} - internal expect fun displayClearTextError(context: FloconContext) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconFile.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconFile.kt index 6fa28d86a..c55899b22 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconFile.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconFile.kt @@ -1,3 +1,6 @@ package io.github.openflocon.flocon -internal expect class FloconFile \ No newline at end of file +import io.github.openflocon.flocon.dsl.FloconMarker + +@FloconMarker +expect class FloconFile \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt index 65aa4bbe0..e91903615 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -1,5 +1,7 @@ package io.github.openflocon.flocon +import io.github.openflocon.flocon.dsl.FloconMarker + /** * Base interface for all Flocon plugins. * Plugins can receive messages from the server and react to connection events. @@ -29,7 +31,8 @@ interface FloconPluginKey { * A factory for creating and installing Flocon plugins. * This is the entry point for Ktor-style [install] calls. */ -interface FloconPluginFactory : FloconPluginKey { +interface FloconPluginFactory : + FloconPluginKey { /** * Create a default configuration instance for the plugin. @@ -37,8 +40,9 @@ interface FloconPluginFactory -) : FloconApp.Client, FloconMessageSender, FloconFileSender { - - private val FLOCON_WEBSOCKET_PORT = 9023 - private val FLOCON_HTTP_PORT = 9024 - - private val appInstance by lazy { currentTimeMillis() } - private val appInfos by lazy { getAppInfos(context) } - private val versionName by lazy { BuildConfig.APP_VERSION } - private val address by lazy { getServerHost(context) } - - private val webSocketClient: FloconWebSocketClient = buildFloconWebSocketClient() - private val httpClient: FloconHttpClient = buildFloconHttpClient() - - private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - - fun getPlugin(key: String): T? { - return plugins.find { it.key == key } as? T - } - - @Throws(Throwable::class) - override suspend fun connect( - onClosed: () -> Unit, - onMessageReceived: (message: String) -> Unit - ) { - webSocketClient.connect( - address = address, - port = FLOCON_WEBSOCKET_PORT, - onMessageReceived = ::onMessageReceived, - onClosed = onClosed, - ) - plugins.forEach { it.onConnectedToServer() } - } - - override suspend fun disconnect() { - webSocketClient.disconnect() - } - - private fun onMessageReceived(message: String) { - coroutineScope.launch(Dispatchers.IO) { - floconMessageFromServerFromJson(message)?.let { messageFromServer -> - messageFromServer.plugin - plugins.find { it.key == messageFromServer.plugin } - ?.onMessageReceived( - method = messageFromServer.method, - body = messageFromServer.body, - ) - } - } - } - - override suspend fun send( - plugin: String, - method: String, - body: String, - ) { - webSocketClient.sendMessage( - message = FloconMessageToServer( - deviceId = appInfos.deviceId, - plugin = plugin, - body = body, - appName = appInfos.appName, - appPackageName = appInfos.appPackageName, - method = method, - deviceName = appInfos.deviceName, - appInstance = appInstance, - platform = appInfos.platform, - versionName = versionName, - ) - .toFloconMessageToServer(), - ) - } - - override suspend fun send( - file: FloconFile, - infos: FloconFileInfo, - ) { - httpClient.send( - address = address, - port = FLOCON_HTTP_PORT, - file = file, - infos = infos, - - deviceId = appInfos.deviceId, - appPackageName = appInfos.appPackageName, - appInstance = appInstance, - ) - } - - override suspend fun sendPendingMessages() { - webSocketClient.sendPendingMessages() - } -} - -internal class FloconClient( +class FloconClient internal constructor( private val context: FloconContext ) : FloconApp.Client, FloconMessageSender, FloconFileSender { @@ -174,6 +69,7 @@ internal class FloconClient( ) } + @FloconMarker override suspend fun send( file: FloconFile, infos: FloconFileInfo, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt index 3f5b977ce..4d835a5a7 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt @@ -1,8 +1,12 @@ package io.github.openflocon.flocon.core import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.model.FloconFileInfo internal interface FloconFileSender { + + @FloconMarker suspend fun send(file: FloconFile, infos: FloconFileInfo) + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconFileInfo.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconFileInfo.kt index 1bd6f4eb8..0aeb75d9d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconFileInfo.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconFileInfo.kt @@ -1,6 +1,9 @@ package io.github.openflocon.flocon.model -internal data class FloconFileInfo( +import io.github.openflocon.flocon.dsl.FloconMarker + +@FloconMarker +data class FloconFileInfo( val path: String, val requestId: String, ) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index d0b293286..d57654c3d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -1,12 +1,11 @@ package io.github.openflocon.flocon.plugins.analytics -import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.analytics.mapper.analyticsItemsToJson import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsConfig import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsPlugin import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem @@ -15,9 +14,12 @@ object FloconAnalytics : FloconPluginFactory { override val name: String = "Files" override val pluginId: String = Protocol.ToDevice.Files.Plugin override fun createConfig() = FloconFilesConfig() - override fun install(config: FloconFilesConfig, app: FloconApp): FloconFilesPlugin { - val client = app.client + override fun install( + pluginConfig: FloconFilesConfig, + floconConfig: FloconConfig + ): FloconFilesPlugin { + val client = floconConfig.client return FloconFilesPluginImpl( - context = app.context, + context = floconConfig.context, floconFileSender = client as FloconFileSender, sender = client as FloconMessageSender ) @@ -37,7 +40,10 @@ object FloconFiles : FloconPluginFactory { } internal interface FileDataSource { + + @FloconMarker fun getFile(path: String, isConstantPath: Boolean): FloconFile? + fun getFolderContent( path: String, isConstantPath: Boolean, @@ -46,6 +52,8 @@ internal interface FileDataSource { fun deleteFile(path: String) fun deleteFiles(path: List) + + @FloconMarker fun deleteFolderContent(folder: FloconFile) } @@ -61,6 +69,7 @@ internal class FloconFilesPluginImpl( private val fileDataSource = fileDataSource(context) private val withFoldersSize = MutableStateFlow(false) + @FloconMarker override suspend fun onMessageReceived( method: String, body: String, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt index 1f5fe1ef5..d64298869 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -10,10 +10,13 @@ object FloconPreferences : FloconPluginFactory { override fun createConfig(): FloconAnalyticsConfig = TODO() - override fun install(config: FloconAnalyticsConfig, app: FloconApp): FloconAnalyticsPlugin = TODO() + override fun install( + pluginConfig: FloconAnalyticsConfig, + floconConfig: FloconConfig + ): FloconAnalyticsPlugin = TODO() override val name: String = "" override val pluginId: String = "ANALYTICS" diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt index 9e5cbabd5..147783416 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.pluginsold.database import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory @@ -17,7 +18,10 @@ object FloconDatabase : FloconPluginFactory { TODO("Not yet implemented") } - override fun install(config: FloconFilesConfig, app: FloconApp): FloconFilesPlugin { + override fun install( + pluginConfig: FloconFilesConfig, + floconConfig: FloconConfig + ): FloconFilesPlugin { TODO("Not yet implemented") } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt index 9af248027..3ec1427d2 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.pluginsold.sharedprefs import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory @@ -17,7 +18,10 @@ object FloconPreferences : FloconPluginFactory { TODO("Not yet implemented") } - override fun install(config: FloconTableConfig, app: FloconApp): FloconTablePlugin { + override fun install( + pluginConfig: FloconTableConfig, + floconConfig: FloconConfig + ): FloconTablePlugin { TODO("Not yet implemented") } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.kt index 7660cf030..a82cb3177 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.kt @@ -1,11 +1,14 @@ package io.github.openflocon.flocon.websocket import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.model.FloconFileInfo -internal expect fun buildFloconHttpClient() : FloconHttpClient +internal expect fun buildFloconHttpClient(): FloconHttpClient internal interface FloconHttpClient { + + @FloconMarker suspend fun send( file: FloconFile, infos: FloconFileInfo, @@ -14,5 +17,5 @@ internal interface FloconHttpClient { deviceId: String, appPackageName: String, appInstance: Long - ) : Boolean + ): Boolean } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/Flocon.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/Flocon.ios.kt deleted file mode 100644 index 14a0608d9..000000000 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/Flocon.ios.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.openflocon.flocon - -object Flocon : FloconCore() { - fun initialize() { - super.initializeFlocon(context = FloconContext()) - } -} - diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/Flocon.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/Flocon.jvm.kt deleted file mode 100644 index ec43cd8ff..000000000 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/Flocon.jvm.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon - -object Flocon : FloconCore() { - - fun initialize() { - super.initializeFlocon(context = FloconContext( - appName = "Flocon-sample", - packageName = "io.github.openflocon.flocon", - )) - } -} diff --git a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.android.kt b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.android.kt new file mode 100644 index 000000000..5c424d559 --- /dev/null +++ b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.android.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.network.core.datasource + +import io.github.openflocon.flocon.FloconContext + +internal actual inline fun buildFloconNetworkDataSource( + context: FloconContext +): FloconNetworkDataSource = FloconNetworkDataSourceAndroid(context = context.context) \ No newline at end of file diff --git a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.android.kt b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt similarity index 90% rename from FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.android.kt rename to FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt index e11512cc2..3351a639f 100644 --- a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.android.kt +++ b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt @@ -1,24 +1,19 @@ -package io.github.openflocon.flocon.network.core +package io.github.openflocon.flocon.network.core.datasource import android.content.Context -import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig import io.github.openflocon.flocon.network.core.mapper.parseMockResponses import io.github.openflocon.flocon.network.core.mapper.toJsonString import io.github.openflocon.flocon.network.core.mapper.writeMockResponsesToJson +import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_BAD_CONFIG_JSON +import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_MOCKS_JSON import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { - return FloconNetworkDataSourceAndroid( - context = context.context, - ) -} - internal class FloconNetworkDataSourceAndroid( private val context: Context ) : FloconNetworkDataSource { diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt new file mode 100644 index 000000000..43f00fd3a --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt @@ -0,0 +1,41 @@ +package io.github.openflocon.flocon.network.core + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.error.pluginNotInitialized +import io.github.openflocon.flocon.network.core.plugin.FloconNetworkPluginImpl +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkConfig +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.SupervisorJob + +object FloconNetwork : FloconPluginFactory { + override val name: String = "Network" + override val pluginId: String = Protocol.ToDevice.Network.Plugin + + override fun createConfig() = FloconNetworkConfig() + + @OptIn(FloconMarker::class) + override fun install( + pluginConfig: FloconNetworkConfig, + floconConfig: FloconConfig + ): FloconNetworkPlugin { + return FloconNetworkPluginImpl( + context = floconConfig.context, + sender = floconConfig.client as FloconMessageSender, + coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + ) + .also { FloconNetworkPluginImpl.plugin = it } + } + +} + +@OptIn(FloconMarker::class) +val Flocon.Companion.networkPlugin: FloconNetworkPlugin + get() = FloconNetworkPluginImpl.plugin ?: pluginNotInitialized("Network") \ No newline at end of file diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/NetworkCore.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/NetworkCore.kt deleted file mode 100644 index 1ce7f0e47..000000000 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/NetworkCore.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.openflocon.flocon.network.core - -// Placeholder for Network Core diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.kt new file mode 100644 index 000000000..69cf9d441 --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.kt @@ -0,0 +1,16 @@ +package io.github.openflocon.flocon.network.core.datasource + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse + +internal interface FloconNetworkDataSource { + fun saveMocksToFile(mocks: List) + fun loadMocksFromFile(): List + fun saveBadNetworkConfig(config: BadQualityConfig?) + fun loadBadNetworkConfig(): BadQualityConfig? +} + +internal expect inline fun buildFloconNetworkDataSource( + context: FloconContext +): FloconNetworkDataSource \ No newline at end of file diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt similarity index 78% rename from FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt rename to FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt index e0057fc06..cca03df51 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt @@ -1,14 +1,11 @@ -package io.github.openflocon.flocon.network.core +package io.github.openflocon.flocon.network.core.plugin -import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.dsl.FloconMarker -import io.github.openflocon.flocon.error.pluginNotInitialized +import io.github.openflocon.flocon.network.core.datasource.buildFloconNetworkDataSource import io.github.openflocon.flocon.network.core.mapper.floconNetworkCallRequestToJson import io.github.openflocon.flocon.network.core.mapper.floconNetworkCallResponseToJson import io.github.openflocon.flocon.network.core.mapper.floconNetworkWebSocketEventToJson @@ -16,7 +13,6 @@ import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig import io.github.openflocon.flocon.network.core.mapper.parseMockResponses import io.github.openflocon.flocon.network.core.mapper.parseWebSocketMockMessage import io.github.openflocon.flocon.network.core.mapper.webSocketIdsToJsonArray -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkConfig import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest @@ -25,45 +21,14 @@ import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketMockListener import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -object FloconNetwork : FloconPluginFactory { - override val name: String = "Network" - override val pluginId: String = Protocol.ToDevice.Network.Plugin - override fun createConfig() = FloconNetworkConfig() - override fun install(config: FloconNetworkConfig, app: FloconApp): FloconNetworkPlugin { - return FloconNetworkPluginImpl( - context = app.context, - sender = app.client as FloconMessageSender, - coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), - ) - .also { FloconNetworkPluginImpl.plugin = it } - } -} - internal const val FLOCON_NETWORK_MOCKS_JSON = "flocon_network_mocks.json" internal const val FLOCON_NETWORK_BAD_CONFIG_JSON = "flocon_network_bad_config.json" -internal interface FloconNetworkDataSource { - fun saveMocksToFile(mocks: List) - fun loadMocksFromFile(): List - fun saveBadNetworkConfig(config: BadQualityConfig?) - fun loadBadNetworkConfig(): BadQualityConfig? -} - -internal expect fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource - -@OptIn(FloconMarker::class) -@Suppress("UnusedReceiverParameter") -val FloconApp.networkPlugin: FloconNetworkPlugin - get() = FloconNetworkPluginImpl.plugin ?: pluginNotInitialized("Network") - internal class FloconNetworkPluginImpl( context: FloconContext, private var sender: FloconMessageSender, diff --git a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.ios.kt b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.ios.kt new file mode 100644 index 000000000..580dd4815 --- /dev/null +++ b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.ios.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.network.core.datasource + +import io.github.openflocon.flocon.FloconContext + +internal actual inline fun buildFloconNetworkDataSource( + context: FloconContext +): FloconNetworkDataSource = FloconNetworkDataSourceImpl() \ No newline at end of file diff --git a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.ios.kt b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt similarity index 62% rename from FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.ios.kt rename to FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt index dc587f51f..fad59bfae 100644 --- a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.ios.kt +++ b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt @@ -1,15 +1,10 @@ -package io.github.openflocon.flocon.network.core +package io.github.openflocon.flocon.network.core.datasource -import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse -internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { - return FloconNetworkDataSourceIOs() -} +internal class FloconNetworkDataSourceImpl : FloconNetworkDataSource { -// TODO -internal class FloconNetworkDataSourceIOs : FloconNetworkDataSource { override fun saveMocksToFile(mocks: List) { // TODO } diff --git a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.jvm.kt b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.jvm.kt new file mode 100644 index 000000000..580dd4815 --- /dev/null +++ b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.jvm.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.network.core.datasource + +import io.github.openflocon.flocon.FloconContext + +internal actual inline fun buildFloconNetworkDataSource( + context: FloconContext +): FloconNetworkDataSource = FloconNetworkDataSourceImpl() \ No newline at end of file diff --git a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.jvm.kt b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt similarity index 91% rename from FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.jvm.kt rename to FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt index 16342afe8..a2e304384 100644 --- a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetworkPluginImpl.jvm.kt +++ b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt @@ -1,22 +1,19 @@ -package io.github.openflocon.flocon.network.core +package io.github.openflocon.flocon.network.core.datasource -import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig import io.github.openflocon.flocon.network.core.mapper.parseMockResponses import io.github.openflocon.flocon.network.core.mapper.toJsonString import io.github.openflocon.flocon.network.core.mapper.writeMockResponsesToJson +import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_BAD_CONFIG_JSON +import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_MOCKS_JSON import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { - return FloconNetworkDataSourceJvm() -} - -internal class FloconNetworkDataSourceJvm( +internal class FloconNetworkDataSourceImpl( ) : FloconNetworkDataSource { private val baseDir: File = File(System.getProperty("user.home"), ".flocon") diff --git a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt index 1986f8f42..d201396fd 100644 --- a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt +++ b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt @@ -2,7 +2,7 @@ package io.github.openflocon.flocon.okhttp -import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.network.core.networkPlugin import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse @@ -29,7 +29,7 @@ class FloconOkhttpInterceptor( @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { - val floconNetworkPlugin = FloconApp.instance!!.networkPlugin + val floconNetworkPlugin = Flocon.networkPlugin if (!shouldLog(chain)) { // on no op, do not intercept the call, just execute it diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/repository/DeeplinkRepositoryImpl.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/repository/DeeplinkRepositoryImpl.kt index 8e8bc6d0b..ed63264f7 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/repository/DeeplinkRepositoryImpl.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/repository/DeeplinkRepositoryImpl.kt @@ -31,8 +31,6 @@ class DeeplinkRepositoryImpl( Protocol.FromDevice.Deeplink.Method.GetDeeplinks -> { val deeplinks = remote.getItems(message) ?: return - println(deeplinks.toString()) - localDeeplinkDataSource.update( deviceIdAndPackageNameDomainModel = deviceIdAndPackageName, deeplinks = deeplinks From cad125e08061b877b295e7abecf9f8df0e77bf5b Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 16:38:03 +0100 Subject: [PATCH 11/38] feat: No op --- .../flocon/network/core/noop/FloconNetwork.kt | 32 +++++ .../network/core/noop/NetworkCoreNoOp.kt | 3 - .../core/noop/mapper/BadQualityToJson.kt | 104 +++++++++++++++ .../noop/mapper/FloconNetworkRequestToJson.kt | 111 ++++++++++++++++ .../core/noop/mapper/MockResponseToJson.kt | 119 ++++++++++++++++++ .../network/core/noop/mapper/Websocket.kt | 25 ++++ .../noop/plugin/FloconNetworkPluginImpl.kt | 32 +++++ FloconAndroid/settings.gradle.kts | 4 +- 8 files changed, 425 insertions(+), 5 deletions(-) create mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt delete mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/NetworkCoreNoOp.kt create mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt create mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt create mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt create mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt create mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/plugin/FloconNetworkPluginImpl.kt diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt new file mode 100644 index 000000000..ab2a5f8c9 --- /dev/null +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt @@ -0,0 +1,32 @@ +package io.github.openflocon.flocon.network.core.noop + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.error.pluginNotInitialized +import io.github.openflocon.flocon.network.core.noop.plugin.FloconNetworkPluginImpl +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkConfig +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin + +object FloconNetwork : FloconPluginFactory { + override val name: String = "Network" + override val pluginId: String = Protocol.ToDevice.Network.Plugin + + override fun createConfig() = FloconNetworkConfig() + + @OptIn(FloconMarker::class) + override fun install( + pluginConfig: FloconNetworkConfig, + floconConfig: FloconConfig + ): FloconNetworkPlugin { + return FloconNetworkPluginImpl() + .also { FloconNetworkPluginImpl.plugin = it } + } + +} + +@OptIn(FloconMarker::class) +val Flocon.Companion.networkPlugin: FloconNetworkPlugin + get() = FloconNetworkPluginImpl.plugin ?: pluginNotInitialized("Network") \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/NetworkCoreNoOp.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/NetworkCoreNoOp.kt deleted file mode 100644 index cf487ea7b..000000000 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/NetworkCoreNoOp.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.openflocon.flocon.network.core.noop - -// Placeholder for Network Core No-Op diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt new file mode 100644 index 000000000..1e9c85456 --- /dev/null +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt @@ -0,0 +1,104 @@ +package io.github.openflocon.flocon.network.core.noop.mapper + +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString + +internal fun BadQualityConfig.toJsonString(): String { + return FloconEncoder.json.encodeToString( + toSerializable() + ) +} + +internal fun parseBadQualityConfig(jsonString: String): BadQualityConfig? { + return try { + val parsed = FloconEncoder.json.decodeFromString( + jsonString + ) + parsed.toDomain() + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "bad connection network parsing issue", t) + null + } +} + +@Serializable +internal class BadQualityConfigSerializable( + val latency: LatencySerializable, + val errorProbability: Double, + val errors: List, +) { + @Serializable + class LatencySerializable( + val latencyTriggerProbability: Float, + val minLatencyMs: Long, + val maxLatencyMs: Long, + ) + + @Serializable + class ErrorSerializable( + val weight: Float, + val errorCode: Int? = null, + val errorBody: String? = null, + val errorContentType: String? = null, + val errorException: String? = null, + ) +} + +internal fun BadQualityConfig.toSerializable(): BadQualityConfigSerializable { + return BadQualityConfigSerializable( + latency = BadQualityConfigSerializable.LatencySerializable( + latencyTriggerProbability = latency.latencyTriggerProbability, + minLatencyMs = latency.minLatencyMs, + maxLatencyMs = latency.maxLatencyMs + ), + errorProbability = errorProbability, + errors = errors.map { error -> + when (val t = error.type) { + is BadQualityConfig.Error.Type.Body -> BadQualityConfigSerializable.ErrorSerializable( + weight = error.weight, + errorCode = t.errorCode, + errorBody = t.errorBody, + errorContentType = t.errorContentType + ) + + is BadQualityConfig.Error.Type.ErrorThrow -> BadQualityConfigSerializable.ErrorSerializable( + weight = error.weight, + errorException = t.classPath + ) + } + } + ) +} + +internal fun BadQualityConfigSerializable.toDomain(): BadQualityConfig { + val latencyConfig = BadQualityConfig.LatencyConfig( + latencyTriggerProbability = latency.latencyTriggerProbability, + minLatencyMs = latency.minLatencyMs, + maxLatencyMs = latency.maxLatencyMs + ) + + val errorsList = errors.map { e -> + val type = if (!e.errorException.isNullOrEmpty()) { + BadQualityConfig.Error.Type.ErrorThrow(e.errorException) + } else { + BadQualityConfig.Error.Type.Body( + errorCode = e.errorCode ?: 0, + errorBody = e.errorBody.orEmpty(), + errorContentType = e.errorContentType.orEmpty() + ) + } + BadQualityConfig.Error( + weight = e.weight, + type = type + ) + } + + return BadQualityConfig( + latency = latencyConfig, + errorProbability = errorProbability, + errors = errorsList + ) +} \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt new file mode 100644 index 000000000..03b132903 --- /dev/null +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt @@ -0,0 +1,111 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package io.github.openflocon.flocon.network.core.noop.mapper + +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@Serializable +internal class FloconNetworkCallRequestRemote( + val floconCallId: String, + val floconNetworkType: String, + val isMocked: Boolean, + + val url: String, + val method: String, + val startTime: Long, + val requestBody: String?, + val requestHeaders: Map, + val requestSize: Long?, +) + +internal fun FloconNetworkCallRequest.floconNetworkCallRequestToJson(): String { + val remoteModel = FloconNetworkCallRequestRemote( + floconCallId = floconCallId, + floconNetworkType = floconNetworkType, + isMocked = isMocked, + url = request.url, + method = request.method, + startTime = request.startTime, + requestBody = request.body, + requestHeaders = request.headers, + requestSize = request.size + ) + return FloconEncoder.json.encodeToString(remoteModel) +} + +@Serializable +internal class FloconNetworkCallResponseRemote( + val floconCallId: String, + val durationMs: Double, + val floconNetworkType: String, + val isMocked: Boolean, + val responseHttpCode: Int?, + val responseGrpcStatus: String?, + val responseContentType: String?, + val responseBody: String?, + val responseSize: Long?, + val responseHeaders: Map, + val requestHeaders: Map?, // we might receive the request headers later if the interceptor is at first position in the http interceptor chain + val responseError: String?, + val isImage: Boolean, +) + +internal fun FloconNetworkCallResponse.floconNetworkCallResponseToJson(): String { + val remoteModel = FloconNetworkCallResponseRemote( + floconCallId = floconCallId, + floconNetworkType = floconNetworkType, + isMocked = isMocked, + durationMs = durationMs, + responseHttpCode = response.httpCode, + responseGrpcStatus = response.grpcStatus, + responseContentType = response.contentType, + responseBody = response.body, + responseHeaders = response.headers, + requestHeaders = response.requestHeaders?.takeIf { + it.isNotEmpty() + }, + responseSize = response.size, + isImage = response.isImage, + responseError = response.error, + ) + + return FloconEncoder.json.encodeToString(remoteModel) +} + +@Serializable +internal class FloconWebSocketEventRemote( + val id: String, + val event: String, + val url: String, + val size: Long, + val timestamp: Long, + val message: String?, + val error: String?, +) + +internal fun FloconWebSocketEvent.floconNetworkWebSocketEventToJson(): String { + val remoteModel = FloconWebSocketEventRemote( + id = Uuid.random().toString(), + event = when (event) { + FloconWebSocketEvent.Event.Closed -> "closed" + FloconWebSocketEvent.Event.Closing -> "closing" + FloconWebSocketEvent.Event.Error -> "error" + FloconWebSocketEvent.Event.ReceiveMessage -> "received" + FloconWebSocketEvent.Event.SendMessage -> "sent" + FloconWebSocketEvent.Event.Open -> "open" + }, + url = websocketUrl, + size = size, + timestamp = timeStamp, + message = message, + error = error?.message + ) + return FloconEncoder.json.encodeToString(remoteModel) +} \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt new file mode 100644 index 000000000..9696d7d1f --- /dev/null +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt @@ -0,0 +1,119 @@ +package io.github.openflocon.flocon.network.core.noop.mapper + +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString + +@Serializable +internal class MockNetworkResponseDataModel( + val expectation: Expectation, + val response: Response, +) { + @Serializable + class Expectation( + val urlPattern: String, // a regex + val method: String, // can be get, post, put, ... or a wildcard * + ) + + @Serializable + class Response( + val httpCode: Int?, + val body: String?, + val mediaType: String?, + val delay: Long?, + val headers: Map?, + val errorException: String?, + ) +} + + +internal fun parseMockResponses(jsonString: String): List { + try { + val remote = + FloconEncoder.json.decodeFromString>(jsonString) + return remote.mapNotNull { + it.toDomain() + } + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "mock network parsing issue", t) + return emptyList() + } +} + +internal fun MockNetworkResponseDataModel.toDomain(): MockNetworkResponse? { + return MockNetworkResponse( + expectation = MockNetworkResponse.Expectation( + urlPattern = expectation.urlPattern, + method = expectation.method, + ), + response = this.mapResponseToDomain() ?: return null + ) +} + +private fun MockNetworkResponseDataModel.mapResponseToDomain(): MockNetworkResponse.Response? { + return response.run { + when { + errorException != null -> MockNetworkResponse.Response.ErrorThrow( + classPath = errorException, + delay = delay ?: 0L, + ) + + httpCode != null -> MockNetworkResponse.Response.Body( + httpCode = httpCode, + body = body ?: "", + delay = delay ?: 0L, + mediaType = mediaType ?: "", + headers = headers ?: emptyMap() + ) + + else -> run { + FloconLogger.logError("error parsing mock response", null) + return@run null + } + } + } +} + + +internal fun writeMockResponsesToJson(mocks: List): String { + return try { + FloconEncoder.json.encodeToString(mocks.map { it.toRemote() }) + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "mock network writing issue", t) + return "[]" + } +} + +private fun MockNetworkResponse.toRemote(): MockNetworkResponseDataModel { + return MockNetworkResponseDataModel( + expectation = MockNetworkResponseDataModel.Expectation( + urlPattern = expectation.urlPattern, + method = expectation.method, + ), + response = mapResponseToRemote(), + ) +} + +private fun MockNetworkResponse.mapResponseToRemote(): MockNetworkResponseDataModel.Response { + return when (val response = this.response) { + is MockNetworkResponse.Response.ErrorThrow -> MockNetworkResponseDataModel.Response( + errorException = response.classPath, + delay = response.delay, + body = null, + headers = null, + httpCode = null, + mediaType = null, + ) + + is MockNetworkResponse.Response.Body -> MockNetworkResponseDataModel.Response( + errorException = null, + delay = response.delay, + body = response.body, + headers = response.headers, + httpCode = response.httpCode, + mediaType = response.mediaType, + ) + } +} diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt new file mode 100644 index 000000000..8cd994774 --- /dev/null +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt @@ -0,0 +1,25 @@ +package io.github.openflocon.flocon.network.core.noop.mapper + +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.core.FloconEncoder +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString + +@Serializable +internal class WebSocketMockMessage( + val id: String, + val message: String, +) + +internal fun webSocketIdsToJsonArray(ids: Collection): String { + return FloconEncoder.json.encodeToString(ids) +} + +internal fun parseWebSocketMockMessage(jsonString: String): WebSocketMockMessage? { + try { + return FloconEncoder.json.decodeFromString(jsonString) + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "mock wesocket network parsing issue", t) + } + return null +} \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/plugin/FloconNetworkPluginImpl.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/plugin/FloconNetworkPluginImpl.kt new file mode 100644 index 000000000..6f1a38e8e --- /dev/null +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/plugin/FloconNetworkPluginImpl.kt @@ -0,0 +1,32 @@ +package io.github.openflocon.flocon.network.core.noop.plugin + +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin +import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent +import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketMockListener +import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse + +internal class FloconNetworkPluginImpl : FloconPlugin, FloconNetworkPlugin { + override val key: String = "NETWORK" + override val mocks: Collection = emptyList() + override val badQualityConfig: BadQualityConfig? = null + + override suspend fun onMessageReceived(method: String, body: String) = Unit // No op + override suspend fun onConnectedToServer() = Unit // No op + + override fun logRequest(request: FloconNetworkCallRequest) = Unit // No op + override fun logResponse(response: FloconNetworkCallResponse) = Unit // No op + + override suspend fun logWebSocket(event: FloconWebSocketEvent) = Unit // No op + override suspend fun registerWebSocketMockListener( + id: String, + listener: FloconWebSocketMockListener + ) = Unit // No op + + companion object { + var plugin: FloconNetworkPlugin? = null + } +} \ No newline at end of file diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index 1009160d5..aeee1139f 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -1,3 +1,5 @@ +rootProject.name = "Flocon Sample App" + pluginManagement { repositories { google() @@ -14,8 +16,6 @@ dependencyResolutionManagement { } } -rootProject.name = "Flocon Sample App" - include(":sample-android-only") include(":sample-multiplatform") include(":flocon") From 004c71358c673fdd35ff43feff678ac214441628 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 17:30:25 +0100 Subject: [PATCH 12/38] feat: Other plugins --- .../database/FloconDatabasePlugin.android.kt | 6 +- .../analytics/FloconAnalyticsPlugin.kt | 11 ++- .../analytics/builder/AnalyticsBuilder.kt | 32 +++++++++ .../analytics/mapper/AnalyticsItemsMapper.kt | 2 +- .../plugins/analytics/model/AnalyticsEvent.kt | 11 +++ .../plugins/analytics/model/AnalyticsItem.kt | 9 +++ .../model/AnalyticsPropertiesConfig.kt | 11 +++ .../FloconCrashReporterPlugin.kt | 20 ++++-- .../plugins/dashboard/FloconDashboardDSL.kt | 10 +-- .../dashboard/FloconDashboardPlugin.kt | 21 ++++-- .../dashboard/builder/ContainerBuilder.kt | 14 ++++ .../dashboard/builder/DashboardBuilder.kt | 21 ++++++ .../plugins/dashboard/builder/FormBuilder.kt | 20 ++++++ .../dashboard/builder/SectionBuilder.kt | 10 +++ .../flocon/plugins/dashboard/dsl/ButtonDsl.kt | 19 +++++ .../plugins/dashboard/dsl/CheckBoxDsl.kt | 21 ++++++ .../plugins/dashboard/dsl/DashboardDsl.kt | 15 ++++ .../flocon/plugins/dashboard/dsl/FormDsl.kt | 22 ++++++ .../flocon/plugins/dashboard/dsl/HtmlDsl.kt | 9 +++ .../plugins/dashboard/dsl/MarkdownDsl.kt | 9 +++ .../plugins/dashboard/dsl/PlainTextDsl.kt | 26 +++++++ .../plugins/dashboard/dsl/SectionDsl.kt | 13 ++++ .../flocon/plugins/dashboard/dsl/TextDsl.kt | 15 ++++ .../flocon/plugins/dashboard/dsl/TextField.kt | 23 ++++++ .../plugins/dashboard/mapper/JsonMapper.kt | 26 +++---- .../plugins/dashboard/model/ContainerType.kt | 6 ++ .../dashboard/model/DashboardConfig.kt | 8 +++ .../plugins/dashboard/model/DashboardScope.kt | 25 +++++++ .../dashboard/model/config/ButtonConfig.kt | 7 ++ .../dashboard/model/config/CheckBoxConfig.kt | 8 +++ .../dashboard/model/config/ContainerConfig.kt | 9 +++ .../dashboard/model/config/ElementConfig.kt | 3 + .../dashboard/model/config/FormConfig.kt | 13 ++++ .../dashboard/model/config/HtmlConfig.kt | 6 ++ .../dashboard/model/config/LabelConfig.kt | 6 ++ .../dashboard/model/config/MarkdownConfig.kt | 6 ++ .../dashboard/model/config/PlainTextConfig.kt | 7 ++ .../dashboard/model/config/SectionConfig.kt | 10 +++ .../dashboard/model/config/TextConfig.kt | 7 ++ .../dashboard/model/config/TextFieldConfig.kt | 9 +++ .../plugins/database/FloconDatabasePlugin.kt | 20 +++--- .../database/model/FloconDatabaseModel.kt | 10 +++ .../plugins/device/FloconDevicePluginImpl.kt | 16 ++++- .../flocon/plugins/files/FloconFilesPlugin.kt | 10 ++- .../sharedprefs/FloconSharedPrefsPlugin.kt | 18 +++-- .../model/FloconSharedPreferenceModel.kt | 5 ++ .../plugins/tables/FloconTablesPlugin.kt | 20 ++++-- .../flocon/plugins/tables/model/TableItem.kt | 21 +++++- .../FloconSharedPrefsPlugin.jvm.kt | 1 - .../flocon/grpc/model/RequestHolder.kt | 2 +- .../flocon/network/core/noop/FloconNetwork.kt | 4 +- .../core/noop/mapper/BadQualityToJson.kt | 2 +- .../noop/mapper/FloconNetworkRequestToJson.kt | 6 +- .../core/noop/mapper/MockResponseToJson.kt | 2 +- .../noop/plugin/FloconNetworkPluginImpl.kt | 14 ++-- .../FloconNetworkDataSourceAndroid.kt | 4 +- .../flocon/network/core/FloconNetwork.kt | 29 +++++++- .../datasource/FloconNetworkDataSource.kt | 4 +- .../network/core/mapper/BadQualityToJson.kt | 2 +- .../core/mapper/FloconNetworkRequestToJson.kt | 6 +- .../network/core/mapper/MockResponseToJson.kt | 2 +- .../network/core/model/BadQualityConfig.kt | 72 +++++++++++++++++++ .../network/core/model/FloconHttpRequest.kt | 23 ++++++ .../core/model/FloconNetworkCallRequest.kt | 8 +++ .../core/model/FloconNetworkCallResponse.kt | 9 +++ .../core/model/FloconWebSocketEvent.kt | 21 ++++++ .../core/model/FloconWebSocketMockListener.kt | 5 ++ .../network/core/model/MockNetworkResponse.kt | 39 ++++++++++ .../core/plugin/FloconNetworkPluginImpl.kt | 14 ++-- .../datasource/FloconNetworkDataSourceImpl.kt | 4 +- .../datasource/FloconNetworkDataSourceImpl.kt | 4 +- .../openflocon/flocon/ktor/BadQuality.kt | 2 +- .../io/github/openflocon/flocon/ktor/Mocks.kt | 4 +- .../openflocon/flocon/okhttp/BadQuality.kt | 2 +- .../github/openflocon/flocon/okhttp/Mock.kt | 4 +- .../flocon/okhttp/OkHttpInterceptor.kt | 8 +-- .../okhttp/websocket/FloconWebSocket.kt | 2 +- .../dashboard/InitializeDashboard.kt | 16 ++--- .../database/InitializeDatabases.kt | 9 ++- .../sharedpreferences/SharedPreferences.kt | 1 - 80 files changed, 845 insertions(+), 126 deletions(-) create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/ContainerBuilder.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/DashboardBuilder.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/FormBuilder.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/SectionBuilder.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/ButtonDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/CheckBoxDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/DashboardDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/FormDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/HtmlDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/MarkdownDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/PlainTextDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/SectionDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextDsl.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextField.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/ContainerType.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardScope.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ButtonConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/CheckBoxConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ContainerConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ElementConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/FormConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/HtmlConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/LabelConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/MarkdownConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/PlainTextConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/SectionConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextFieldConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/BadQualityConfig.kt create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconHttpRequest.kt create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconNetworkCallRequest.kt create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconNetworkCallResponse.kt create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconWebSocketEvent.kt create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconWebSocketMockListener.kt create mode 100644 FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/MockNetworkResponse.kt diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt index ec7c7ba2f..e8b491b09 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt @@ -24,7 +24,7 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : private val MAX_DEPTH = 7 override fun executeSQL( - registeredDatabases: List, + registeredDatabases: List, databaseName: String, query: String ): DatabaseExecuteSqlResponse { @@ -105,9 +105,7 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : } } - override fun getAllDataBases( - registeredDatabases: List - ): List { + override fun getAllDataBases(registeredDatabases: List): List { val databasesDir = context.getDatabasePath("dummy_db").parentFile ?: return emptyList() val foundDatabases = mutableListOf() diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index d57654c3d..2c79458ae 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -3,12 +3,17 @@ package io.github.openflocon.flocon.plugins.analytics import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsConfig -import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem + +class FloconAnalyticsConfig : FloconPluginConfig + +interface FloconAnalyticsPlugin : FloconPlugin { + fun registerAnalytics(analyticsItems: List) +} object FloconAnalytics : FloconPluginFactory { override val name: String = "Analytics" diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt new file mode 100644 index 000000000..d4aef5b4d --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt @@ -0,0 +1,32 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package io.github.openflocon.flocon.plugins.analytics.builder + +import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.utils.currentTimeMillis +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +class AnalyticsBuilder( + val analyticsTableId: String, + private val analyticsPlugin: FloconAnalyticsPlugin?, +) { + fun logEvents(vararg events: AnalyticsEvent) { + this.logEvents(events.toList()) + } + + fun logEvents(events: List) { + val analyticsItems = events.map { + AnalyticsItem( + id = Uuid.random().toString(), + analyticsTableId = analyticsTableId, + eventName = it.eventName, + createdAt = currentTimeMillis(), + properties = it.properties, + ) + } + analyticsPlugin?.registerAnalytics(analyticsItems) + } +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt index 4a4fae0ed..f93b42598 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt @@ -1,7 +1,7 @@ package io.github.openflocon.flocon.plugins.analytics.mapper import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt new file mode 100644 index 000000000..b0de88ffa --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsEvent( + val eventName: String, + val properties: List, +) { + constructor( + eventName: String, + vararg properties: AnalyticsPropertiesConfig, + ) : this(eventName, properties.toList()) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt new file mode 100644 index 000000000..55c7285cd --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsItem( + val id: String, + val analyticsTableId: String, + val eventName: String, + val createdAt: Long, + val properties: List, +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt new file mode 100644 index 000000000..f36d72382 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsPropertiesConfig( + val name: String, + val value: String, +) + +infix fun String.analyticsProperty(value: String) = AnalyticsPropertiesConfig( + name = this, + value = value, +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt index 39fc9912a..4c31686df 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt @@ -1,10 +1,14 @@ package io.github.openflocon.flocon.plugins.crashreporter -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel -import io.github.openflocon.flocon.pluginsold.crashreporter.FloconCrashReporterConfig -import io.github.openflocon.flocon.pluginsold.crashreporter.FloconCrashReporterPlugin import io.github.openflocon.flocon.utils.currentTimeMillis import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -14,6 +18,14 @@ import kotlinx.coroutines.launch import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +class FloconCrashReporterConfig : FloconPluginConfig { + var catchFatalErrors: Boolean = true +} + +interface FloconCrashReporterPlugin : FloconPlugin { + fun setupCrashHandler() +} + object FloconCrashReporter : FloconPluginFactory { override val name: String = "CrashReporter" @@ -27,7 +39,7 @@ object FloconCrashReporter : ): FloconCrashReporterPlugin { val client = floconConfig.client as FloconMessageSender return FloconCrashReporterPluginImpl( - context = TODO(), //FloconContext(appContext = null), // Handled by datasource + context = floconConfig.context, sender = client, coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), ) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardDSL.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardDSL.kt index 3c9bc8353..f747dd047 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardDSL.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardDSL.kt @@ -1,10 +1,10 @@ package io.github.openflocon.flocon.plugins.dashboard -import io.github.openflocon.flocon.pluginsold.dashboard.builder.FormBuilder -import io.github.openflocon.flocon.pluginsold.dashboard.builder.SectionBuilder -import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardScope -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ContainerConfig +import io.github.openflocon.flocon.plugins.dashboard.builder.FormBuilder +import io.github.openflocon.flocon.plugins.dashboard.builder.SectionBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig +import io.github.openflocon.flocon.plugins.dashboard.model.DashboardScope +import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt index aa60be205..a556a3880 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt @@ -1,21 +1,30 @@ package io.github.openflocon.flocon.plugins.dashboard -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.plugins.dashboard.mapper.toJson import io.github.openflocon.flocon.plugins.dashboard.model.DashboardCallback +import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig import io.github.openflocon.flocon.plugins.dashboard.model.todevice.ToDeviceCheckBoxValueChangedMessage import io.github.openflocon.flocon.plugins.dashboard.model.todevice.ToDeviceSubmittedFormMessage import io.github.openflocon.flocon.plugins.dashboard.model.todevice.ToDeviceSubmittedTextFieldMessage -import io.github.openflocon.flocon.pluginsold.dashboard.FloconDashboardConfig -import io.github.openflocon.flocon.pluginsold.dashboard.FloconDashboardPlugin -import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch - object FloconDashboard : FloconPluginFactory { +class FloconDashboardConfig : FloconPluginConfig + +interface FloconDashboardPlugin : FloconPlugin { + fun registerDashboard(dashboardConfig: DashboardConfig) +} + +object FloconDashboard : FloconPluginFactory { override val name: String = "Dashboard" override val pluginId: String = Protocol.ToDevice.Dashboard.Plugin override fun createConfig() = FloconDashboardConfig() @@ -41,7 +50,7 @@ internal class FloconDashboardPluginImpl( private val dashboards = mutableMapOf() private val callbackMap = mutableMapOf() - override suspend fun onMessageReceived( + override suspend fun onMessageReceived( method: String, body: String, ) { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/ContainerBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/ContainerBuilder.kt new file mode 100644 index 000000000..e9685eb39 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/ContainerBuilder.kt @@ -0,0 +1,14 @@ +package io.github.openflocon.flocon.plugins.dashboard.builder + +import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.ElementConfig + +abstract class ContainerBuilder { + open val elements = mutableListOf() + + open fun add(element: ElementConfig) { + elements.add(element) + } + + abstract fun build(): ContainerConfig +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/DashboardBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/DashboardBuilder.kt new file mode 100644 index 000000000..77c7352b9 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/DashboardBuilder.kt @@ -0,0 +1,21 @@ +package io.github.openflocon.flocon.plugins.dashboard.builder + +import io.github.openflocon.flocon.plugins.dashboard.dsl.DashboardDsl +import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig + +@DashboardDsl +class DashboardBuilder(private val id: String) { + private val containers = mutableListOf() + + fun add(container: ContainerConfig) { + containers.add(container) + } + + fun build(): DashboardConfig { + return DashboardConfig( + id = id, + containers = containers + ) + } +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/FormBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/FormBuilder.kt new file mode 100644 index 000000000..71ecf7b7d --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/FormBuilder.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.flocon.plugins.dashboard.builder + +import io.github.openflocon.flocon.plugins.dashboard.model.config.FormConfig + +class FormBuilder( + val name: String, + val submitText: String, + val onSubmitted: (Map) -> Unit, +) : ContainerBuilder() { + + override fun build(): FormConfig { + return FormConfig( + id = "form_$name", + name = name, + submitText = submitText, + elements = elements, + onSubmitted = onSubmitted + ) + } +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/SectionBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/SectionBuilder.kt new file mode 100644 index 000000000..4d7cde6d6 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/builder/SectionBuilder.kt @@ -0,0 +1,10 @@ +package io.github.openflocon.flocon.plugins.dashboard.builder + +import io.github.openflocon.flocon.plugins.dashboard.model.config.SectionConfig + +class SectionBuilder(val name: String) : ContainerBuilder() { + + override fun build(): SectionConfig { + return SectionConfig(name, elements) + } +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/ButtonDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/ButtonDsl.kt new file mode 100644 index 000000000..60f4fac25 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/ButtonDsl.kt @@ -0,0 +1,19 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.config.ButtonConfig + +@DashboardDsl +fun ContainerBuilder.button( + text: String, + id: String, + onClick: () -> Unit, +) { + add( + ButtonConfig( + text = text, + id = id, + onClick = onClick, + ) + ) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/CheckBoxDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/CheckBoxDsl.kt new file mode 100644 index 000000000..6569df699 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/CheckBoxDsl.kt @@ -0,0 +1,21 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.config.CheckBoxConfig + +@DashboardDsl +fun ContainerBuilder.checkBox( + id: String, + label: String, + value: Boolean, + onUpdated: (Boolean) -> Unit = {}, +) { + add( + CheckBoxConfig( + id = id, + label = label, + value = value, + onUpdated = onUpdated, + ) + ) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/DashboardDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/DashboardDsl.kt new file mode 100644 index 000000000..097eab050 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/DashboardDsl.kt @@ -0,0 +1,15 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.DashboardBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig + +@DslMarker +annotation class DashboardDsl + +fun dashboardConfig(id: String, block: DashboardBuilder.() -> Unit): DashboardConfig { + val builder = DashboardBuilder(id = id) + .apply { + block() + } + return builder.build() +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/FormDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/FormDsl.kt new file mode 100644 index 000000000..9603e49ec --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/FormDsl.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.DashboardBuilder +import io.github.openflocon.flocon.plugins.dashboard.builder.FormBuilder + +@DashboardDsl +fun DashboardBuilder.form( + name: String, + submitText: String, + onSubmitted: (Map) -> Unit, + block: FormBuilder.() -> Unit +) { + val builder = FormBuilder( + name = name, + submitText = submitText, + onSubmitted = onSubmitted + ).apply { + block() + } + + add(builder.build()) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/HtmlDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/HtmlDsl.kt new file mode 100644 index 000000000..5b93e4884 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/HtmlDsl.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.config.HtmlConfig + +@DashboardDsl +fun ContainerBuilder.html(label: String, value: String) { + add(HtmlConfig(label = label, value = value)) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/MarkdownDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/MarkdownDsl.kt new file mode 100644 index 000000000..1a9d5f6f3 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/MarkdownDsl.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.config.MarkdownConfig + +@DashboardDsl +fun ContainerBuilder.markdown(label: String, value: String) { + add(MarkdownConfig(label = label, value = value)) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/PlainTextDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/PlainTextDsl.kt new file mode 100644 index 000000000..8fa25c287 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/PlainTextDsl.kt @@ -0,0 +1,26 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.config.PlainTextConfig + +@DashboardDsl +fun ContainerBuilder.plainText(label: String, value: String) { + add( + PlainTextConfig( + label = label, + value = value, + type = "text", + ) + ) +} + +@DashboardDsl +fun ContainerBuilder.json(label: String, value: String) { + add( + PlainTextConfig( + label = label, + value = value, + type = "json", + ) + ) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/SectionDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/SectionDsl.kt new file mode 100644 index 000000000..0bc1b3956 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/SectionDsl.kt @@ -0,0 +1,13 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.DashboardBuilder +import io.github.openflocon.flocon.plugins.dashboard.builder.SectionBuilder + +@DashboardDsl +fun DashboardBuilder.section(name: String, block: SectionBuilder.() -> Unit) { + val builder = SectionBuilder(name).apply { + block() + } + + add(builder.build()) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextDsl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextDsl.kt new file mode 100644 index 000000000..c56b3f555 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextDsl.kt @@ -0,0 +1,15 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.config.LabelConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.TextConfig + +@DashboardDsl +fun ContainerBuilder.text(label: String, value: String, color: Int? = null) { + add(TextConfig(label = label, value = value, color = color)) +} + +@DashboardDsl +fun ContainerBuilder.label(label: String, color: Int? = null) { + add(LabelConfig(label = label, color = color)) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextField.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextField.kt new file mode 100644 index 000000000..42bf0bc4e --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/dsl/TextField.kt @@ -0,0 +1,23 @@ +package io.github.openflocon.flocon.plugins.dashboard.dsl + +import io.github.openflocon.flocon.plugins.dashboard.builder.ContainerBuilder +import io.github.openflocon.flocon.plugins.dashboard.model.config.TextFieldConfig + +@DashboardDsl +fun ContainerBuilder.textField( + id: String, + label: String, + placeHolder: String?, + value: String, + onSubmitted: (String) -> Unit = {}, +) { + add( + TextFieldConfig( + id = id, + label = label, + placeHolder = placeHolder, + value = value, + onSubmitted = onSubmitted, + ) + ) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/mapper/JsonMapper.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/mapper/JsonMapper.kt index c7410bcb3..09626f9cc 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/mapper/JsonMapper.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/mapper/JsonMapper.kt @@ -7,19 +7,19 @@ import io.github.openflocon.flocon.plugins.dashboard.model.DashboardCallback.But import io.github.openflocon.flocon.plugins.dashboard.model.DashboardCallback.CheckBoxCallback import io.github.openflocon.flocon.plugins.dashboard.model.DashboardCallback.FormCallback import io.github.openflocon.flocon.plugins.dashboard.model.DashboardCallback.TextFieldCallback -import io.github.openflocon.flocon.pluginsold.dashboard.model.DashboardConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ButtonConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.CheckBoxConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ContainerConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.ElementConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.FormConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.HtmlConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.LabelConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.MarkdownConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.PlainTextConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.SectionConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.TextConfig -import io.github.openflocon.flocon.pluginsold.dashboard.model.config.TextFieldConfig +import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.ButtonConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.CheckBoxConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.ElementConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.FormConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.HtmlConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.LabelConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.MarkdownConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.PlainTextConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.SectionConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.TextConfig +import io.github.openflocon.flocon.plugins.dashboard.model.config.TextFieldConfig import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/ContainerType.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/ContainerType.kt new file mode 100644 index 000000000..8bf2247aa --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/ContainerType.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.plugins.dashboard.model + +enum class ContainerType { + FORM, + SECTION +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardConfig.kt new file mode 100644 index 000000000..87432e05a --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardConfig.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocon.plugins.dashboard.model + +import io.github.openflocon.flocon.plugins.dashboard.model.config.ContainerConfig + +data class DashboardConfig( + val id: String, + val containers: List +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardScope.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardScope.kt new file mode 100644 index 000000000..291222091 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/DashboardScope.kt @@ -0,0 +1,25 @@ +package io.github.openflocon.flocon.plugins.dashboard.model + +import io.github.openflocon.flocon.plugins.dashboard.builder.FormBuilder +import io.github.openflocon.flocon.plugins.dashboard.builder.SectionBuilder +import kotlinx.coroutines.flow.Flow + +interface DashboardScope { + fun section(name: String, flow: Flow, content: SectionBuilder.(T) -> Unit) + fun section(name: String, content: SectionBuilder.() -> Unit) + + fun form( + name: String, + submitText: String = "Submit", + onSubmitted: (Map) -> Unit, + flow: Flow, + content: FormBuilder.(T) -> Unit + ) + + fun form( + name: String, + submitText: String = "Submit", + onSubmitted: (Map) -> Unit, + content: FormBuilder.() -> Unit + ) +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ButtonConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ButtonConfig.kt new file mode 100644 index 000000000..f7703e279 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ButtonConfig.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +data class ButtonConfig( + val text: String, + val id: String, + val onClick: () -> Unit, +) : ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/CheckBoxConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/CheckBoxConfig.kt new file mode 100644 index 000000000..f6f534486 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/CheckBoxConfig.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +data class CheckBoxConfig( + val id: String, + val label: String, + val value: Boolean, + val onUpdated: (Boolean) -> Unit, +) : ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ContainerConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ContainerConfig.kt new file mode 100644 index 000000000..88f77c091 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ContainerConfig.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +import io.github.openflocon.flocon.plugins.dashboard.model.ContainerType + +sealed interface ContainerConfig { + val name: String + val elements: List + val containerType: ContainerType +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ElementConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ElementConfig.kt new file mode 100644 index 000000000..8a3e4e3c0 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/ElementConfig.kt @@ -0,0 +1,3 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +sealed interface ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/FormConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/FormConfig.kt new file mode 100644 index 000000000..37e3bf917 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/FormConfig.kt @@ -0,0 +1,13 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +import io.github.openflocon.flocon.plugins.dashboard.model.ContainerType + +data class FormConfig( + override val name: String, + override val elements: List, + val id: String, + val submitText: String, + val onSubmitted: (Map) -> Unit, +) : ContainerConfig { + override val containerType: ContainerType = ContainerType.FORM +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/HtmlConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/HtmlConfig.kt new file mode 100644 index 000000000..70fce9994 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/HtmlConfig.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +data class HtmlConfig( + val label: String, + val value: String, +) : ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/LabelConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/LabelConfig.kt new file mode 100644 index 000000000..9dcf4e815 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/LabelConfig.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +data class LabelConfig( + val label: String, + val color: Int?, +) : ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/MarkdownConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/MarkdownConfig.kt new file mode 100644 index 000000000..f2e4a3799 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/MarkdownConfig.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +data class MarkdownConfig( + val label: String, + val value: String, +) : ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/PlainTextConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/PlainTextConfig.kt new file mode 100644 index 000000000..12b74553f --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/PlainTextConfig.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +data class PlainTextConfig( + val label: String, + val value: String, + val type: String, // text, json +) : ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/SectionConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/SectionConfig.kt new file mode 100644 index 000000000..a707d5b01 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/SectionConfig.kt @@ -0,0 +1,10 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +import io.github.openflocon.flocon.plugins.dashboard.model.ContainerType + +data class SectionConfig( + override val name: String, + override val elements: List, +) : ContainerConfig { + override val containerType: ContainerType = ContainerType.SECTION +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextConfig.kt new file mode 100644 index 000000000..b33db66ba --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextConfig.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +data class TextConfig( + val label: String, + val value: String, + val color: Int?, +) : ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextFieldConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextFieldConfig.kt new file mode 100644 index 000000000..63de1f656 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/config/TextFieldConfig.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.plugins.dashboard.model.config + +data class TextFieldConfig( + val id: String, + val label: String, + val placeHolder: String?, + val value: String, + val onSubmitted: (String) -> Unit, +) : ElementConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt index 1d9ceac05..0463c2d9f 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt @@ -1,21 +1,26 @@ package io.github.openflocon.flocon.plugins.database -import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel import io.github.openflocon.flocon.plugins.database.model.todevice.DatabaseQueryMessage -import io.github.openflocon.flocon.pluginsold.database.FloconDatabaseConfig -import io.github.openflocon.flocon.pluginsold.database.FloconDatabasePlugin -import io.github.openflocon.flocon.pluginsold.database.model.FloconDatabaseModel +import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel import kotlinx.coroutines.flow.MutableStateFlow +class FloconDatabaseConfig : FloconPluginConfig + +interface FloconDatabasePlugin : FloconPlugin { + fun register(floconDatabaseModel: FloconDatabaseModel) + fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) +} + internal interface FloconDatabaseDataSource { fun executeSQL( registeredDatabases: List, @@ -40,7 +45,7 @@ object FloconDatabase : FloconPluginFactory { override val name: String = "Device" diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt index 152b8e666..a3c708973 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt @@ -1,11 +1,11 @@ package io.github.openflocon.flocon.plugins.files -import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconFile import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconFileSender @@ -17,11 +17,15 @@ import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceDeleteFi import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceDeleteFolderContentMessage import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceGetFileMessage import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceGetFilesMessage -import io.github.openflocon.flocon.pluginsold.files.FloconFilesConfig -import io.github.openflocon.flocon.pluginsold.files.FloconFilesPlugin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +class FloconFilesConfig : FloconPluginConfig { + val roots = mutableListOf() +} + +interface FloconFilesPlugin : FloconPlugin + object FloconFiles : FloconPluginFactory { override val name: String = "Files" override val pluginId: String = Protocol.ToDevice.Files.Plugin diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt index d64298869..f2d162865 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -1,10 +1,20 @@ package io.github.openflocon.flocon.plugins.sharedprefs -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.pluginsold.sharedprefs.FloconPreferencesConfig -import io.github.openflocon.flocon.pluginsold.sharedprefs.FloconPreferencesPlugin -import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconSharedPreferenceModel +import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconSharedPreferenceModel + +class FloconPreferencesConfig : FloconPluginConfig + +interface FloconPreferencesPlugin : FloconPlugin { + fun register(sharedPreference: FloconSharedPreferenceModel) +} object FloconPreferences : FloconPluginFactory { override val name: String = "Preferences" diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt new file mode 100644 index 000000000..47b0e946f --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt @@ -0,0 +1,5 @@ +package io.github.openflocon.flocon.plugins.sharedprefs.model + +// TODO Get model from git +class FloconSharedPreferenceModel { +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt index 338bce055..e22068da7 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt @@ -1,12 +1,22 @@ package io.github.openflocon.flocon.plugins.tables -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.pluginsold.tables.FloconTableConfig -import io.github.openflocon.flocon.pluginsold.tables.FloconTablePlugin -import io.github.openflocon.flocon.pluginsold.tables.model.TableItem +import io.github.openflocon.flocon.plugins.tables.model.TableItem +import io.github.openflocon.flocon.plugins.tables.model.tableItemListToJson - object FloconTable : FloconPluginFactory { +class FloconTableConfig : FloconPluginConfig + +interface FloconTablePlugin : FloconPlugin { + fun registerItems(tableItems: List) +} + +object FloconTable : FloconPluginFactory { override val name: String = "Table" override val pluginId: String = Protocol.ToDevice.Table.Plugin override fun createConfig() = FloconTableConfig() diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt index 2e81530b7..81467899b 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt @@ -1,11 +1,28 @@ package io.github.openflocon.flocon.plugins.tables.model import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.tables.model.TableColumnConfig -import io.github.openflocon.flocon.pluginsold.tables.model.TableItem import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString +data class TableItem( + val id: String, + val name: String, + val createdAt: Long, + val columns: List, +) + +data class TableColumnConfig( + val columnName: String, + val value: String, +) + +infix fun String.toParam(value: String) = TableColumnConfig( + columnName = this, + value = value, +) + +// --- JSON Serialization --- + internal fun tableItemListToJson(items: Collection): String { return FloconEncoder.json.encodeToString(items.map { it.toRemote() }) } diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt index 645d38c33..9d596dd9a 100644 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt +++ b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt @@ -2,7 +2,6 @@ package io.github.openflocon.flocon.plugins.sharedprefs import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference -import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference //internal actual fun buildFloconPreferencesDataSource(context: FloconContext): FloconPreferencesDataSource { // return FloconPreferencesDataSourceJvm() diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt index 2f9de14b2..00d93ba8b 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt @@ -4,5 +4,5 @@ import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest import kotlinx.coroutines.CompletableDeferred internal data class RequestHolder( - val request: CompletableDeferred = CompletableDeferred() + val request: CompletableDeferred = CompletableDeferred() ) \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt index ab2a5f8c9..4935b04a4 100644 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt @@ -7,8 +7,8 @@ import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.error.pluginNotInitialized import io.github.openflocon.flocon.network.core.noop.plugin.FloconNetworkPluginImpl -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkConfig -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin +import io.github.openflocon.flocon.network.core.FloconNetworkConfig +import io.github.openflocon.flocon.network.core.FloconNetworkPlugin object FloconNetwork : FloconPluginFactory { override val name: String = "Network" diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt index 1e9c85456..90df55c94 100644 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt @@ -2,7 +2,7 @@ package io.github.openflocon.flocon.network.core.noop.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.BadQualityConfig import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt index 03b132903..4fe64d4db 100644 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt @@ -3,9 +3,9 @@ package io.github.openflocon.flocon.network.core.noop.mapper import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.network.core.model.FloconWebSocketEvent import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlin.uuid.ExperimentalUuidApi diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt index 9696d7d1f..7ab3d09bf 100644 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt @@ -2,7 +2,7 @@ package io.github.openflocon.flocon.network.core.noop.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/plugin/FloconNetworkPluginImpl.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/plugin/FloconNetworkPluginImpl.kt index 6f1a38e8e..a2b02b316 100644 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/plugin/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/plugin/FloconNetworkPluginImpl.kt @@ -1,13 +1,13 @@ package io.github.openflocon.flocon.network.core.noop.plugin import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketMockListener -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.FloconNetworkPlugin +import io.github.openflocon.flocon.network.core.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.network.core.model.FloconWebSocketEvent +import io.github.openflocon.flocon.network.core.model.FloconWebSocketMockListener +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse internal class FloconNetworkPluginImpl : FloconPlugin, FloconNetworkPlugin { override val key: String = "NETWORK" diff --git a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt index 3351a639f..07a3bd15a 100644 --- a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt +++ b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt @@ -8,8 +8,8 @@ import io.github.openflocon.flocon.network.core.mapper.toJsonString import io.github.openflocon.flocon.network.core.mapper.writeMockResponsesToJson import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_BAD_CONFIG_JSON import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_MOCKS_JSON -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse import java.io.File import java.io.FileInputStream import java.io.FileOutputStream diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt index 43f00fd3a..636aada5e 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt @@ -2,19 +2,44 @@ package io.github.openflocon.flocon.network.core import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.error.pluginNotInitialized +import io.github.openflocon.flocon.network.core.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.network.core.model.FloconWebSocketEvent +import io.github.openflocon.flocon.network.core.model.FloconWebSocketMockListener +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse import io.github.openflocon.flocon.network.core.plugin.FloconNetworkPluginImpl -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkConfig -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.SupervisorJob +class FloconNetworkConfig : FloconPluginConfig { + var badQualityConfig: BadQualityConfig? = null + val mocks = mutableListOf() +} + +interface FloconNetworkPlugin : FloconPlugin { + val mocks: Collection + val badQualityConfig: BadQualityConfig? + + fun logRequest(request: FloconNetworkCallRequest) + fun logResponse(response: FloconNetworkCallResponse) + + suspend fun logWebSocket( + event: FloconWebSocketEvent, + ) + + suspend fun registerWebSocketMockListener(id: String, listener: FloconWebSocketMockListener) +} + object FloconNetwork : FloconPluginFactory { override val name: String = "Network" override val pluginId: String = Protocol.ToDevice.Network.Plugin diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.kt index 69cf9d441..4048ace9c 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.kt @@ -1,8 +1,8 @@ package io.github.openflocon.flocon.network.core.datasource import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse internal interface FloconNetworkDataSource { fun saveMocksToFile(mocks: List) diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/BadQualityToJson.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/BadQualityToJson.kt index 0b4c0950e..88caef4df 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/BadQualityToJson.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/BadQualityToJson.kt @@ -2,7 +2,7 @@ package io.github.openflocon.flocon.network.core.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.BadQualityConfig import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt index 45d3977cd..329115850 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt @@ -3,9 +3,9 @@ package io.github.openflocon.flocon.network.core.mapper import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.network.core.model.FloconWebSocketEvent import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlin.uuid.ExperimentalUuidApi diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt index ba7227eed..76b00f959 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt @@ -2,7 +2,7 @@ package io.github.openflocon.flocon.network.core.mapper import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/BadQualityConfig.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/BadQualityConfig.kt new file mode 100644 index 000000000..fb1293f90 --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/BadQualityConfig.kt @@ -0,0 +1,72 @@ +package io.github.openflocon.flocon.network.core.model + +import io.github.openflocon.flocon.FloconLogger +import kotlin.random.Random + +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, + ) { + fun shouldSimulateLatency(): Boolean { + return latencyTriggerProbability > 0f && (latencyTriggerProbability == 1f || Random.nextDouble() < latencyTriggerProbability) + } + fun getRandomLatency() : Long { + return Random.nextLong( + minLatencyMs, + maxLatencyMs + 1 + ) + } + } + class Error( + val weight: Float, // increase the probability of being triggered vs all others errors + val type: Type, + ) { + sealed interface Type { + data class Body( + val errorCode: Int, + val errorBody: String, + val errorContentType: String, // "application/json" + ) : Type + data class ErrorThrow( + val classPath: String, + ) : Type { + fun generate() : Throwable? { + return try { + io.github.openflocon.flocon.utils.createThrowableFromClassName(classPath) + } catch (t: Throwable) { + FloconLogger.logError("BadQualityConfig error, className not found", t) + null + } + } + } + } + } + + fun shouldFail(): Boolean { + return errorProbability > 0 && Random.nextDouble() < errorProbability + } + + fun selectRandomError(): BadQualityConfig.Error? { + if (errors.isEmpty()) { + return null + } + + val totalWeight = errors.sumOf { it.weight.toDouble() } + var randomNumber = Random.nextDouble(0.0, totalWeight) + + for (error in errors) { + randomNumber -= error.weight.toDouble() + if (randomNumber <= 0) { + return error + } + } + + return errors.first() + } +} diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconHttpRequest.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconHttpRequest.kt new file mode 100644 index 000000000..2b2029b59 --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconHttpRequest.kt @@ -0,0 +1,23 @@ +package io.github.openflocon.flocon.network.core.model + +data class FloconNetworkRequest( + val url: String, + val method: String, + val startTime: Long, + val headers: Map, + val body: String?, + val size: Long?, + val isMocked: Boolean, +) + +data class FloconNetworkResponse( + val httpCode: Int?, + val grpcStatus: String?, + val contentType: String?, + val body: String?, + val size: Long?, + val headers: Map, + val requestHeaders: Map?, // we might receive the request headers later if the interceptor is at first position in the http interceptor chain + val error: String?, + val isImage: Boolean, +) diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconNetworkCallRequest.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconNetworkCallRequest.kt new file mode 100644 index 000000000..bcaf31628 --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconNetworkCallRequest.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocon.network.core.model + +data class FloconNetworkCallRequest( + val floconCallId: String, + val request: FloconNetworkRequest, + val floconNetworkType: String, + val isMocked: Boolean, +) diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconNetworkCallResponse.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconNetworkCallResponse.kt new file mode 100644 index 000000000..20cb62bf3 --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconNetworkCallResponse.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.network.core.model + +data class FloconNetworkCallResponse( + val floconCallId: String, + val response: FloconNetworkResponse, + val durationMs: Double, + val floconNetworkType: String, + val isMocked: Boolean, +) diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconWebSocketEvent.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconWebSocketEvent.kt new file mode 100644 index 000000000..df16fbbc4 --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconWebSocketEvent.kt @@ -0,0 +1,21 @@ +package io.github.openflocon.flocon.network.core.model + +import io.github.openflocon.flocon.utils.currentTimeMillis + +class FloconWebSocketEvent( + val websocketUrl: String, + val event: Event, + val size: Long = 0L, + val message: String? = null, + val error: Throwable? = null, + val timeStamp: Long = currentTimeMillis(), +) { + enum class Event { + Closed, + Closing, + Error, + ReceiveMessage, + SendMessage, + Open, + } +} diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconWebSocketMockListener.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconWebSocketMockListener.kt new file mode 100644 index 000000000..3c1928a7f --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/FloconWebSocketMockListener.kt @@ -0,0 +1,5 @@ +package io.github.openflocon.flocon.network.core.model + +interface FloconWebSocketMockListener { + fun onMessage(message: String) +} diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/MockNetworkResponse.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/MockNetworkResponse.kt new file mode 100644 index 000000000..09562fe3c --- /dev/null +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/model/MockNetworkResponse.kt @@ -0,0 +1,39 @@ +package io.github.openflocon.flocon.network.core.model + +data class MockNetworkResponse( + val expectation: Expectation, + val response: Response, +) { + data class Expectation( + val urlPattern: String, // a regex + val method: String, // can be get, post, put, ... or a wildcard * + ) { + + private val regex = Regex(urlPattern) + + fun matches(url: String, method: String): Boolean { + val urlMatches = regex.matches(url) + val methodMatches = this.method == "*" || this.method.equals(method, ignoreCase = true) + return urlMatches && methodMatches + } + } + + sealed interface Response { + val delay: Long + data class Body( + val httpCode: Int, + val body: String, + override val delay: Long, + val mediaType: String, + val headers: Map, + ) : Response + data class ErrorThrow( + val classPath: String, + override val delay: Long, + ) : Response { + fun generate() : Throwable? { + return io.github.openflocon.flocon.utils.createThrowableFromClassName(classPath) + } + } + } +} diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt index cca03df51..c95e76a53 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt @@ -13,13 +13,13 @@ import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig import io.github.openflocon.flocon.network.core.mapper.parseMockResponses import io.github.openflocon.flocon.network.core.mapper.parseWebSocketMockMessage import io.github.openflocon.flocon.network.core.mapper.webSocketIdsToJsonArray -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketMockListener -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.FloconNetworkPlugin +import io.github.openflocon.flocon.network.core.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.network.core.model.FloconWebSocketEvent +import io.github.openflocon.flocon.network.core.model.FloconWebSocketMockListener +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow diff --git a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt index fad59bfae..c062ee677 100644 --- a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt +++ b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt @@ -1,7 +1,7 @@ package io.github.openflocon.flocon.network.core.datasource -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse internal class FloconNetworkDataSourceImpl : FloconNetworkDataSource { diff --git a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt index a2e304384..be864eafa 100644 --- a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt +++ b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt @@ -7,8 +7,8 @@ import io.github.openflocon.flocon.network.core.mapper.toJsonString import io.github.openflocon.flocon.network.core.mapper.writeMockResponsesToJson import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_BAD_CONFIG_JSON import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_MOCKS_JSON -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse import java.io.File import java.io.FileInputStream import java.io.FileOutputStream diff --git a/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt b/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt index 0ee37502c..b56f7fd25 100644 --- a/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt +++ b/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/BadQuality.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.ktor -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.BadQualityConfig import io.ktor.client.HttpClient import io.ktor.client.call.HttpClientCall import io.ktor.client.request.HttpRequestBuilder diff --git a/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt b/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt index d6d99e3b7..1f68c28e5 100644 --- a/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt +++ b/FloconAndroid/network/ktor-interceptor/src/commonMain/kotlin/io/github/openflocon/flocon/ktor/Mocks.kt @@ -1,7 +1,7 @@ package io.github.openflocon.flocon.ktor -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.FloconNetworkPlugin +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse import io.ktor.client.HttpClient import io.ktor.client.call.HttpClientCall import io.ktor.client.request.HttpRequestBuilder diff --git a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt index 35c443ef4..b632a7e9e 100644 --- a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt +++ b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/BadQuality.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.okhttp -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.BadQualityConfig import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Protocol import okhttp3.Request diff --git a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt index b09e3f3ac..a597b6702 100644 --- a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt +++ b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Mock.kt @@ -1,7 +1,7 @@ package io.github.openflocon.flocon.okhttp -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.FloconNetworkPlugin +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Protocol import okhttp3.Request diff --git a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt index d201396fd..3d0b712ba 100644 --- a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt +++ b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt @@ -4,10 +4,10 @@ package io.github.openflocon.flocon.okhttp import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.network.core.networkPlugin -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkResponse +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.network.core.model.FloconNetworkRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkResponse import okhttp3.Interceptor import okhttp3.MediaType import okhttp3.Request diff --git a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt index d7208acbf..09d5593fb 100644 --- a/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt +++ b/FloconAndroid/network/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt @@ -1,7 +1,7 @@ package io.github.openflocon.flocon.okhttp.websocket import io.github.openflocon.flocon.FloconApp -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent +import io.github.openflocon.flocon.network.core.model.FloconWebSocketEvent import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt index bc472fa48..e0761a5d8 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt @@ -12,14 +12,14 @@ import io.github.openflocon.flocon.myapplication.dashboard.device.initializeDevi import io.github.openflocon.flocon.myapplication.dashboard.tokens.tokensFlow import io.github.openflocon.flocon.myapplication.dashboard.user.userFlow import io.github.openflocon.flocon.plugins.dashboard.floconDashboard -import io.github.openflocon.flocon.pluginsold.dashboard.dsl.button -import io.github.openflocon.flocon.pluginsold.dashboard.dsl.checkBox -import io.github.openflocon.flocon.pluginsold.dashboard.dsl.html -import io.github.openflocon.flocon.pluginsold.dashboard.dsl.json -import io.github.openflocon.flocon.pluginsold.dashboard.dsl.markdown -import io.github.openflocon.flocon.pluginsold.dashboard.dsl.plainText -import io.github.openflocon.flocon.pluginsold.dashboard.dsl.text -import io.github.openflocon.flocon.pluginsold.dashboard.dsl.textField +import io.github.openflocon.flocon.plugins.dashboard.dsl.button +import io.github.openflocon.flocon.plugins.dashboard.dsl.checkBox +import io.github.openflocon.flocon.plugins.dashboard.dsl.html +import io.github.openflocon.flocon.plugins.dashboard.dsl.json +import io.github.openflocon.flocon.plugins.dashboard.dsl.markdown +import io.github.openflocon.flocon.plugins.dashboard.dsl.plainText +import io.github.openflocon.flocon.plugins.dashboard.dsl.text +import io.github.openflocon.flocon.plugins.dashboard.dsl.textField import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt index b14bb0625..c0da1d077 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt @@ -6,7 +6,6 @@ import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.database.model.FoodEntity import io.github.openflocon.flocon.myapplication.database.model.HumanEntity import io.github.openflocon.flocon.myapplication.database.model.HumanWithDogEntity -import io.github.openflocon.flocon.pluginsold.database.floconRegisterDatabase import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -15,10 +14,10 @@ fun initializeInMemoryDatabases(context: Context): DogDatabase { context, DogDatabase::class.java, ).build().also { - floconRegisterDatabase( - displayName = "inmemory_dogs", - openHelper = it.openHelper - ) +// floconRegisterDatabase( +// displayName = "inmemory_dogs", +// openHelper = it.openHelper +// ) } } diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt index 051be10c0..099d6d9f6 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt @@ -5,7 +5,6 @@ package io.github.openflocon.flocon.myapplication.sharedpreferences import android.content.Context import android.preference.PreferenceManager import androidx.core.content.edit -import io.github.openflocon.flocon.pluginsold.sharedprefs.FloconSharedPreference import org.json.JSONArray import org.json.JSONObject import kotlin.uuid.Uuid From c4125f5f73aea9dbc2b1a6e09c4bdb98b05451ae Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Thu, 12 Mar 2026 23:40:07 +0100 Subject: [PATCH 13/38] feat: Move database --- .../database/core-no-op/build.gradle.kts | 118 ++++++++++++++ FloconAndroid/database/core/build.gradle.kts | 126 +++++++++++++++ .../core}/FloconDatabasePlugin.android.kt | 57 ++----- .../core/FloconSqliteDatabaseModel.kt | 9 ++ .../database/core/FloconDatabasePlugin.kt | 146 ++++++++++++++++++ .../core/model/FloconDatabaseModel.kt | 16 ++ .../fromdevice/DatabaseExecuteSqlResponse.kt | 59 +++++++ .../model/fromdevice/DatabaseQueryLogModel.kt | 23 +++ .../fromdevice/DeviceDataBaseDataModel.kt | 15 ++ .../QueryResultReceivedDataModel.kt | 15 ++ .../model/todevice/DatabaseQueryMessage.kt | 23 +++ .../database/core/FloconDatabasePlugin.ios.kt | 127 +++++++++++++++ .../database/room-no-op/build.gradle.kts | 82 ++++++++++ .../database/room/floconRegisterDatabase.kt | 9 ++ FloconAndroid/database/room/build.gradle.kts | 91 +++++++++++ .../database/room/FloconRoomDatabaseModel.kt | 28 ++++ FloconAndroid/flocon/build.gradle.kts | 5 - .../database/FloconDatabasePlugin.android.kt | 7 - .../database/FloconSqliteDatabaseModel.kt | 27 ---- .../plugins/database/FloconDatabasePlugin.kt | 136 +--------------- .../database/model/FloconDatabaseModel.kt | 11 +- .../database/FloconDatabasePlugin.kt | 39 +---- .../database/model/FloconDatabaseModel.kt | 11 +- FloconAndroid/settings.gradle.kts | 4 + 24 files changed, 908 insertions(+), 276 deletions(-) create mode 100644 FloconAndroid/database/core-no-op/build.gradle.kts create mode 100644 FloconAndroid/database/core/build.gradle.kts rename FloconAndroid/{flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database => database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core}/FloconDatabasePlugin.android.kt (78%) create mode 100644 FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconSqliteDatabaseModel.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseQueryLogModel.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DeviceDataBaseDataModel.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/QueryResultReceivedDataModel.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/todevice/DatabaseQueryMessage.kt create mode 100644 FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt create mode 100644 FloconAndroid/database/room-no-op/build.gradle.kts create mode 100644 FloconAndroid/database/room-no-op/src/main/java/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt create mode 100644 FloconAndroid/database/room/build.gradle.kts create mode 100644 FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt delete mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.android.kt delete mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconSqliteDatabaseModel.kt diff --git a/FloconAndroid/database/core-no-op/build.gradle.kts b/FloconAndroid/database/core-no-op/build.gradle.kts new file mode 100644 index 000000000..b7390b113 --- /dev/null +++ b/FloconAndroid/database/core-no-op/build.gradle.kts @@ -0,0 +1,118 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "io.github.openflocon.flocon.database.core.noop" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-database-core-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Database Core No-Op" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/database/core/build.gradle.kts b/FloconAndroid/database/core/build.gradle.kts new file mode 100644 index 000000000..295d31de3 --- /dev/null +++ b/FloconAndroid/database/core/build.gradle.kts @@ -0,0 +1,126 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + api(project(":flocon")) + + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(libs.kotlinx.serialization.json) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.androidx.sqlite) + implementation(libs.androidx.sqlite.framework) + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + dependencies { + implementation(libs.androidx.sqlite.bundled) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.database.core" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-database-core", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Database Core" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.android.kt similarity index 78% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt rename to FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.android.kt index e8b491b09..a35064604 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.android.kt +++ b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.android.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.pluginsold.database +package io.github.openflocon.flocon.database.core import android.content.Context import android.database.Cursor @@ -7,16 +7,16 @@ import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.database.FloconDatabaseDataSource -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel -import io.github.openflocon.flocon.pluginsold.database.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.FloconAndroidSqlDatabaseModel +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.DeviceDataBaseDataModel import java.io.File import java.util.Locale -//internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { -// return FloconDatabaseDataSourceAndroid(context.context) -//} +internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { + return FloconDatabaseDataSourceAndroid(context.context) +} internal class FloconDatabaseDataSourceAndroid(private val context: Context) : FloconDatabaseDataSource { @@ -24,18 +24,19 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : private val MAX_DEPTH = 7 override fun executeSQL( - registeredDatabases: List, + registeredDatabases: List, databaseName: String, query: String ): DatabaseExecuteSqlResponse { val databaseModel = registeredDatabases.find { it.displayName == databaseName } - return when(databaseModel) { - is FloconSqliteDatabaseModel -> { + return when (databaseModel) { + is FloconAndroidSqlDatabaseModel -> { executeSQL( database = databaseModel.database, query = query, ) } + else -> openDbAndExecuteQuery( databaseName = databaseName, query = query, @@ -105,7 +106,7 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : } } - override fun getAllDataBases(registeredDatabases: List): List { + override fun getAllDataBases(registeredDatabases: List): List { val databasesDir = context.getDatabasePath("dummy_db").parentFile ?: return emptyList() val foundDatabases = mutableListOf() @@ -116,39 +117,9 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : foundDatabases = foundDatabases ) -// registeredDatabases.forEach { -// when(it) { -// is FloconFileDatabaseModel -> { -// // check if file exists here -// if (File(it.absolutePath).exists()) { -// foundDatabases.add( -// DeviceDataBaseDataModel( -// id = it.absolutePath, -// name = it.displayName, -// ) -// ) -// } -// } -// else -> { -// foundDatabases.add( -// DeviceDataBaseDataModel( -// id = it.displayName, -// name = it.displayName, -// ) -// ) -// } -// } -// } - return foundDatabases } - /** - * Recursively scans a directory for SQLite database files. - * - * @param directory The current directory to scan. - * @param foundDatabases The mutable list to add found databases to. - */ private fun scanDirectoryForDatabases( directory: File, depth: Int, @@ -184,7 +155,6 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : } } - private fun executeSelect( database: SupportSQLiteDatabase, query: String, @@ -259,7 +229,6 @@ private fun getObjectFromColumnIndex(cursor: Cursor, column: Int): String? { } } -// must use the old way to get the version... private fun getDatabaseVersion( path: String, ): Int { diff --git a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconSqliteDatabaseModel.kt b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconSqliteDatabaseModel.kt new file mode 100644 index 000000000..a5001434c --- /dev/null +++ b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconSqliteDatabaseModel.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.database.core + +import androidx.sqlite.db.SupportSQLiteDatabase +import io.github.openflocon.flocon.database.core.model.FloconAndroidSqlDatabaseModel + +internal class FloconSqliteDatabaseModel( + override val displayName: String, + val database: SupportSQLiteDatabase +) : FloconAndroidSqlDatabaseModel diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt new file mode 100644 index 000000000..296894eae --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt @@ -0,0 +1,146 @@ +package io.github.openflocon.flocon.database.core + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.DeviceDataBaseDataModel +import io.github.openflocon.flocon.database.core.model.todevice.DatabaseQueryMessage +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.error.pluginNotInitialized +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +class FloconDatabaseConfig : FloconPluginConfig + +interface FloconDatabasePlugin : FloconPlugin { + fun register(floconDatabaseModel: FloconDatabaseModel) + fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) +} + +internal interface FloconDatabaseDataSource { + fun executeSQL( + registeredDatabases: List, + databaseName: String, + query: String + ): DatabaseExecuteSqlResponse + + fun getAllDataBases( + registeredDatabases: List + ): List +} + +internal expect fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource + +object FloconDatabase : FloconPluginFactory { + override val name: String = "Database" + override val pluginId: String = Protocol.ToDevice.Database.Plugin + override fun createConfig() = FloconDatabaseConfig() + override fun install( + pluginConfig: FloconDatabaseConfig, + floconConfig: FloconConfig + ): FloconDatabasePlugin { + return FloconDatabasePluginImpl( + sender = floconConfig.client as FloconMessageSender, + context = floconConfig.context + ).also { FloconDatabasePluginImpl.plugin = it } + } +} + +internal class FloconDatabasePluginImpl( + private var sender: FloconMessageSender, + private val context: FloconContext, +) : FloconPlugin, FloconDatabasePlugin { + override val key: String = "DATABASE" + + companion object { + var plugin: FloconDatabasePlugin? = null + } + + private val registeredDatabases = MutableStateFlow>(emptyList()) + + private val dataSource = buildFloconDatabaseDataSource(context) + + override suspend fun onMessageReceived( + method: String, + body: String, + ) { + when (method) { + Protocol.ToDevice.Database.Method.GetDatabases -> { + sendAllDatabases(sender) + } + + Protocol.ToDevice.Database.Method.Query -> { + val queryMessage = + DatabaseQueryMessage.fromJson(message = body) ?: return + val result = dataSource.executeSQL( + registeredDatabases = registeredDatabases.value, + databaseName = queryMessage.database, + query = queryMessage.query, + ) + try { +// sender.send( +// plugin = Protocol.FromDevice.Database.Plugin, +// method = Protocol.FromDevice.Database.Method.Query, +// body = QueryResultDataModel( +// requestId = queryMessage.requestId, +// result = result.toJson(), +// ).toJson(), +// ) + } catch (t: Throwable) { + FloconLogger.logError("Database parsing error", t) + } + } + } + } + + override suspend fun onConnectedToServer() { + sendAllDatabases(sender) + } + + private fun sendAllDatabases(sender: FloconMessageSender) { + val databases = dataSource.getAllDataBases( + registeredDatabases = registeredDatabases.value, + ) + try { +// sender.send( +// plugin = Protocol.FromDevice.Database.Plugin, +// method = Protocol.FromDevice.Database.Method.GetDatabases, +// body = listDeviceDataBaseDataModelToJson(databases), +// ) + } catch (t: Throwable) { + FloconLogger.logError("Database parsing error", t) + } + } + + override fun register(floconDatabaseModel: FloconDatabaseModel) { + registeredDatabases.update { it + floconDatabaseModel } + } + + override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) { + try { +// sender.send( +// plugin = Protocol.FromDevice.Database.Plugin, +// method = Protocol.FromDevice.Database.Method.LogQuery, +// body = DatabaseQueryLogModel( +// dbName = dbName, +// sqlQuery = sqlQuery, +// bindArgs = bindArgs.map { it.toString() }, +// timestamp = currentTimeMillis(), +// ).toJson(), +// ) + } catch (t: Throwable) { + FloconLogger.logError("Database logging error", t) + } + } +} + +@OptIn(FloconMarker::class) +val Flocon.Companion.databasePlugin: FloconDatabasePlugin + get() = FloconDatabasePluginImpl.plugin ?: pluginNotInitialized("Database") diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt new file mode 100644 index 000000000..7b33e8822 --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt @@ -0,0 +1,16 @@ +package io.github.openflocon.flocon.database.core.model + +import androidx.sqlite.db.SupportSQLiteDatabase + +interface FloconDatabaseModel { + val displayName: String +} + +interface FloconAndroidSqlDatabaseModel : FloconDatabaseModel { + val database: SupportSQLiteDatabase +} + +data class FloconFileDatabaseModel( + override val displayName: String, + val absolutePath: String +) : FloconDatabaseModel diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt new file mode 100644 index 000000000..71a71e9d9 --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt @@ -0,0 +1,59 @@ +package io.github.openflocon.flocon.database.core.model.fromdevice + +import io.github.openflocon.flocon.core.FloconEncoder +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.put + +@Serializable +sealed interface DatabaseExecuteSqlResponse { + + @Serializable + // Case for successful SELECT queries + class Select( + val columns: List, + val values: List> + ) : DatabaseExecuteSqlResponse + + // Case for successful INSERT queries + @Serializable + class Insert( + val insertedId: Long + ) : DatabaseExecuteSqlResponse + + // Case for successful UPDATE or DELETE queries + @Serializable + class UpdateDelete( + val affectedCount: Int + ) : DatabaseExecuteSqlResponse + + // Case for successful "raw" queries (CREATE TABLE, DROP TABLE, etc.) + @Serializable + object RawSuccess : DatabaseExecuteSqlResponse + + // Case for an SQL execution error + @Serializable + class Error( + val message: String, // Detailed error message + val originalSql: String, // SQL query that caused the error (optional) + ) : DatabaseExecuteSqlResponse +} + +fun DatabaseExecuteSqlResponse.toJson(): String { + val jsonEncoder = FloconEncoder.json + val thisAsJson = jsonEncoder.encodeToJsonElement(this) + + val type = when (this) { + is DatabaseExecuteSqlResponse.Error -> "Error" + is DatabaseExecuteSqlResponse.Insert -> "Insert" + DatabaseExecuteSqlResponse.RawSuccess -> "RawSuccess" + is DatabaseExecuteSqlResponse.Select -> "Select" + is DatabaseExecuteSqlResponse.UpdateDelete -> "UpdateDelete" + } + + return buildJsonObject { + put("type", type) + put("body", thisAsJson.toString()) // warning : the desktop is waiting for a string representation of the json here + }.toString() +} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseQueryLogModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseQueryLogModel.kt new file mode 100644 index 000000000..74359152f --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseQueryLogModel.kt @@ -0,0 +1,23 @@ +package io.github.openflocon.flocon.database.core.model.fromdevice + +import io.github.openflocon.flocon.core.FloconEncoder +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString + +@Serializable +data class DatabaseQueryLogModel( + val dbName: String, + val sqlQuery: String, + val bindArgs: List?, + val timestamp: Long, +) { + fun toJson(): String { + return FloconEncoder.json.encodeToString(this) + } + + companion object { + fun fromJson(json: String): DatabaseQueryLogModel { + return FloconEncoder.json.decodeFromString(json) + } + } +} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DeviceDataBaseDataModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DeviceDataBaseDataModel.kt new file mode 100644 index 000000000..42e00113b --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DeviceDataBaseDataModel.kt @@ -0,0 +1,15 @@ +package io.github.openflocon.flocon.database.core.model.fromdevice + +import io.github.openflocon.flocon.core.FloconEncoder +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString + +@Serializable +data class DeviceDataBaseDataModel( + val id: String, + val name: String, +) + +fun listDeviceDataBaseDataModelToJson(items: List) : String { + return FloconEncoder.json.encodeToString(items) +} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/QueryResultReceivedDataModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/QueryResultReceivedDataModel.kt new file mode 100644 index 000000000..c4e51d071 --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/QueryResultReceivedDataModel.kt @@ -0,0 +1,15 @@ +package io.github.openflocon.flocon.database.core.model.fromdevice + +import io.github.openflocon.flocon.core.FloconEncoder +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString + +@Serializable +internal data class QueryResultDataModel( + val requestId: String, + val result: String, +) { + fun toJson(): String { + return FloconEncoder.json.encodeToString(this) + } +} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/todevice/DatabaseQueryMessage.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/todevice/DatabaseQueryMessage.kt new file mode 100644 index 000000000..cd81313c8 --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/todevice/DatabaseQueryMessage.kt @@ -0,0 +1,23 @@ +package io.github.openflocon.flocon.database.core.model.todevice + +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.core.FloconEncoder +import kotlinx.serialization.Serializable + +@Serializable +data class DatabaseQueryMessage( + val query: String, + val requestId: String, + val database: String, +) { + companion object { + fun fromJson(message: String): DatabaseQueryMessage? { + return try { + FloconEncoder.json.decodeFromString(message) + } catch (t: Throwable) { + FloconLogger.logError("parsing issue", t) + null + } + } + } +} diff --git a/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt b/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt new file mode 100644 index 000000000..f0645b467 --- /dev/null +++ b/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt @@ -0,0 +1,127 @@ +package io.github.openflocon.flocon.database.core + +import androidx.sqlite.SQLiteConnection +import androidx.sqlite.driver.NativeSQLiteDriver +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.DeviceDataBaseDataModel +import platform.Foundation.NSFileManager + +internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { + return FloconDatabaseDataSourceIos(context) +} + +internal class FloconDatabaseDataSourceIos( + private val context: FloconContext +) : FloconDatabaseDataSource { + + override fun executeSQL( + registeredDatabases: List, + databaseName: String, + query: String + ): DatabaseExecuteSqlResponse { + val fileManager = NSFileManager.defaultManager + if (!fileManager.fileExistsAtPath(databaseName)) { + return DatabaseExecuteSqlResponse.Error( + message = "Database file not found: $databaseName", + originalSql = query + ) + } + + val driver = NativeSQLiteDriver() + val connection = driver.open(fileName = databaseName) + + return try { + val firstWord = getFirstWord(query).uppercase() + when (firstWord) { + "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) + "INSERT" -> executeInsert(connection, query) + "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) + else -> executeRawQuery(connection, query) + } + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "Error executing SQL", + originalSql = query + ) + } finally { + connection.close() + } + } + + override fun getAllDataBases( + registeredDatabases: List + ): List { + val fileManager = NSFileManager.defaultManager + return registeredDatabases.mapNotNull { + if(it is FloconFileDatabaseModel) { + if (fileManager.fileExistsAtPath(it.absolutePath)) { + DeviceDataBaseDataModel( + id = it.absolutePath, + name = it.displayName + ) + } else null + } else null + } + } +} + +// --- SQL execution helpers --- + +private fun executeSelect(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + val cursor = connection.prepare(query).use { statement -> + val columnCount = statement.getColumnCount() + val columns = (0 until columnCount).map { statement.getColumnName(it) } + val rows = mutableListOf>() + + while (statement.step()) { + val row = (0 until columnCount).map { idx -> + statement.getText(idx) + } + rows.add(row) + } + + statement.close() // maybe remove + DatabaseExecuteSqlResponse.Select(columns, rows) + } + return cursor +} + +private fun executeUpdateDelete(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { statement -> + statement.close() + } + // sqlite-kt n'expose pas encore `changes()`, on renvoie 0 + return DatabaseExecuteSqlResponse.UpdateDelete(affectedCount = 0) +} + +private fun executeInsert(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { statement -> + statement.close() + } + + // Récupération du dernier ID inséré + var id = -1L + connection.prepare("SELECT last_insert_rowid()").use { +// id = if (it.step()) it.getLong(0) else -1L +// it.close() // maybe remove + } + + return DatabaseExecuteSqlResponse.Insert(id) +} + +private fun executeRawQuery(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { statement -> + statement.close() // maybe remove + } + return DatabaseExecuteSqlResponse.RawSuccess +} + +// --- Utilities --- +private fun getFirstWord(s: String): String { + val trimmed = s.trim() + val firstSpace = trimmed.indexOf(' ') + return if (firstSpace >= 0) trimmed.substring(0, firstSpace) else trimmed +} diff --git a/FloconAndroid/database/room-no-op/build.gradle.kts b/FloconAndroid/database/room-no-op/build.gradle.kts new file mode 100644 index 000000000..b5a556424 --- /dev/null +++ b/FloconAndroid/database/room-no-op/build.gradle.kts @@ -0,0 +1,82 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + id("com.vanniktech.maven.publish") version "0.34.0" +} + +android { + namespace = "io.github.openflocon.flocon.database.room.noop" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(project(":database:core-no-op")) +} + + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-database-room-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Room Implementation No-Op" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/database/room-no-op/src/main/java/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt b/FloconAndroid/database/room-no-op/src/main/java/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt new file mode 100644 index 000000000..1aefa1497 --- /dev/null +++ b/FloconAndroid/database/room-no-op/src/main/java/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.database.room + +fun floconRegisterDatabase(displayName: String, database: Any) { + // no op +} + +fun floconRegisterDatabase(displayName: String, openHelper: Any, dummy: Boolean = true) { + // no op +} diff --git a/FloconAndroid/database/room/build.gradle.kts b/FloconAndroid/database/room/build.gradle.kts new file mode 100644 index 000000000..b60b75333 --- /dev/null +++ b/FloconAndroid/database/room/build.gradle.kts @@ -0,0 +1,91 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + id("com.vanniktech.maven.publish") version "0.34.0" +} + +android { + namespace = "io.github.openflocon.flocon.database.room" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + api(project(":database:core")) + + implementation(libs.androidx.room.runtime) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.jetbrains.kotlinx.coroutines.core) + implementation(libs.jetbrains.kotlinx.coroutines.android) + + testImplementation(libs.junit) +} + + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-database-room", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Room implementation" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt new file mode 100644 index 000000000..b826dda27 --- /dev/null +++ b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt @@ -0,0 +1,28 @@ +package io.github.openflocon.flocon.database.room + +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.database.core.databasePlugin +import io.github.openflocon.flocon.database.core.model.FloconAndroidSqlDatabaseModel + +data class FloconRoomDatabaseModel( + override val displayName: String, + val database: SupportSQLiteDatabase +) : FloconAndroidSqlDatabaseModel + +fun floconRegisterDatabase(displayName: String, database: SupportSQLiteDatabase) { + Flocon.databasePlugin.register( + FloconRoomDatabaseModel( + displayName = displayName, + database = database, + ) + ) +} + +fun floconRegisterDatabase(displayName: String, openHelper: SupportSQLiteOpenHelper) { + floconRegisterDatabase( + displayName = displayName, + database = openHelper.writableDatabase, + ) +} diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index 457438cef..3c5e9f5ad 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -38,9 +38,6 @@ kotlin { implementation(libs.kotlinx.coroutines.android) implementation(libs.jakewharton.process.phoenix) implementation("com.squareup.okhttp3:okhttp:4.12.0") - - implementation(libs.androidx.sqlite) - implementation(libs.androidx.sqlite.framework) } } @@ -71,8 +68,6 @@ kotlin { implementation(libs.ktor.client.logging) implementation(libs.ktor.serialization.kotlinx.json) - implementation(libs.androidx.sqlite.bundled) - // to store the device id implementation("com.russhwolf:multiplatform-settings:1.3.0") } diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.android.kt deleted file mode 100644 index 8a4ac38fb..000000000 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.android.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.openflocon.flocon.plugins.database - -import io.github.openflocon.flocon.FloconContext - -internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { - TODO("Not yet implemented") -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconSqliteDatabaseModel.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconSqliteDatabaseModel.kt deleted file mode 100644 index b5b0087b9..000000000 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconSqliteDatabaseModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.database - -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import io.github.openflocon.flocon.pluginsold.database.model.FloconDatabaseModel - -internal class FloconSqliteDatabaseModel( - override val displayName: String, - val database: SupportSQLiteDatabase -) : FloconDatabaseModel - -fun floconRegisterDatabase(displayName: String, database: SupportSQLiteDatabase) { -// floconRegisterDatabase( -// FloconSqliteDatabaseModel( -// displayName = displayName, -// database = database, -// ) -// ) -} - - -fun floconRegisterDatabase(displayName: String, openHelper: SupportSQLiteOpenHelper) { - floconRegisterDatabase( - displayName = displayName, - database = openHelper.writableDatabase, - ) -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt index 0463c2d9f..1d9511610 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt @@ -1,135 +1 @@ -package io.github.openflocon.flocon.plugins.database - -import io.github.openflocon.flocon.FloconConfig -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.FloconPluginFactory -import io.github.openflocon.flocon.Protocol -import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel -import io.github.openflocon.flocon.plugins.database.model.todevice.DatabaseQueryMessage -import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel -import kotlinx.coroutines.flow.MutableStateFlow - -class FloconDatabaseConfig : FloconPluginConfig - -interface FloconDatabasePlugin : FloconPlugin { - fun register(floconDatabaseModel: FloconDatabaseModel) - fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) -} - -internal interface FloconDatabaseDataSource { - fun executeSQL( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteSqlResponse - - fun getAllDataBases( - registeredDatabases: List - ): List -} - -internal expect fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource - -object FloconDatabase : FloconPluginFactory { - override val name: String = "Database" - override val pluginId: String = Protocol.ToDevice.Database.Plugin - override fun createConfig() = FloconDatabaseConfig() - override fun install( - pluginConfig: FloconDatabaseConfig, - floconConfig: FloconConfig - ): FloconDatabasePlugin { - return FloconDatabasePluginImpl( - sender = floconConfig.client as FloconMessageSender, - context = floconConfig.context - ) - } -} - -internal class FloconDatabasePluginImpl( - private var sender: FloconMessageSender, - private val context: FloconContext, -) : FloconPlugin, FloconDatabasePlugin { - override val key: String = "DATABASE" - - private val registeredDatabases = MutableStateFlow>(emptyList()) - - private val dataSource = buildFloconDatabaseDataSource(context) - - override suspend fun onMessageReceived( - method: String, - body: String, - ) { - when (method) { - Protocol.ToDevice.Database.Method.GetDatabases -> { - sendAllDatabases(sender) - } - - Protocol.ToDevice.Database.Method.Query -> { - val queryMessage = - DatabaseQueryMessage.fromJson(message = body) ?: return - val result = dataSource.executeSQL( - registeredDatabases = registeredDatabases.value, - databaseName = queryMessage.database, - query = queryMessage.query, - ) - try { -// sender.send( -// plugin = Protocol.FromDevice.Database.Plugin, -// method = Protocol.FromDevice.Database.Method.Query, -// body = QueryResultDataModel( -// requestId = queryMessage.requestId, -// result = result.toJson(), -// ).toJson(), -// ) - } catch (t: Throwable) { - FloconLogger.logError("Database parsing error", t) - } - } - } - } - - override suspend fun onConnectedToServer() { - sendAllDatabases(sender) - } - - private fun sendAllDatabases(sender: FloconMessageSender) { - val databases = dataSource.getAllDataBases( - registeredDatabases = registeredDatabases.value, - ) - try { -// sender.send( -// plugin = Protocol.FromDevice.Database.Plugin, -// method = Protocol.FromDevice.Database.Method.GetDatabases, -// body = listDeviceDataBaseDataModelToJson(databases), -// ) - } catch (t: Throwable) { - FloconLogger.logError("Database parsing error", t) - } - } - - override fun register(floconDatabaseModel: FloconDatabaseModel) { - TODO("Not yet implemented") - } - - override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) { - try { -// sender.send( -// plugin = Protocol.FromDevice.Database.Plugin, -// method = Protocol.FromDevice.Database.Method.LogQuery, -// body = DatabaseQueryLogModel( -// dbName = dbName, -// sqlQuery = sqlQuery, -// bindArgs = bindArgs.map { it.toString() }, -// timestamp = currentTimeMillis(), -// ).toJson(), -// ) - } catch (t: Throwable) { - FloconLogger.logError("Database logging error", t) - } - } -} \ No newline at end of file +// DEPRECATED: Moved to :database:core \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt index 06d08c830..dc137eed6 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt @@ -1,10 +1 @@ -package io.github.openflocon.flocon.plugins.database.model - -interface FloconDatabaseModel { - val displayName: String -} - -data class FloconFileDatabaseModel( - override val displayName: String, - val absolutePath: String -) : FloconDatabaseModel +// DEPRECATED diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt index 147783416..1d9511610 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt @@ -1,38 +1 @@ -package io.github.openflocon.flocon.pluginsold.database - -import io.github.openflocon.flocon.FloconApp -import io.github.openflocon.flocon.FloconConfig -import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.FloconPluginFactory -import io.github.openflocon.flocon.pluginsold.database.model.FloconDatabaseModel - -class FloconDatabaseConfig : FloconPluginConfig - -/** - * Flocon Database Plugin. - * Used to inspect Room or other SQL databases. - */ -object FloconDatabase : FloconPluginFactory { - override fun createConfig(): FloconDatabaseConfig { - TODO("Not yet implemented") - } - - override fun install( - pluginConfig: FloconDatabaseConfig, - floconConfig: FloconConfig - ): FloconDatabasePlugin { - TODO("Not yet implemented") - } - - override val name: String - get() = TODO("Not yet implemented") - override val pluginId: String - get() = TODO("Not yet implemented") -} - - -interface FloconDatabasePlugin : FloconPlugin { - fun register(floconDatabaseModel: FloconDatabaseModel) - fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) -} \ No newline at end of file +// DEPRECATED: Moved to :database:core \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt index 4298eb2eb..0447420ba 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt @@ -1,10 +1 @@ -package io.github.openflocon.flocon.pluginsold.database.model - -interface FloconDatabaseModel { - val displayName: String -} - -data class FloconFileDatabaseModel( - override val displayName: String, - val absolutePath: String -) : FloconDatabaseModel \ No newline at end of file +// DEPRECATED \ No newline at end of file diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index aeee1139f..1a9e42680 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -33,3 +33,7 @@ include(":deeplinks") include(":deeplinks-no-op") include(":network:core") include(":network:core-no-op") +include(":database:core") +include(":database:core-no-op") +include(":database:room") +include(":database:room-no-op") From f56cb5ad5a9a2300904112c6e5515ce14fda9abf Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Fri, 13 Mar 2026 00:07:11 +0100 Subject: [PATCH 14/38] fix: Compile --- FloconAndroid/database/core/build.gradle.kts | 3 - .../core/FloconSqliteDatabaseModel.kt | 9 -- .../database/core/FloconDatabasePlugin.kt | 29 +++-- .../core/model/FloconDatabaseModel.kt | 6 +- .../database/core/FloconDatabasePlugin.jvm.kt | 7 ++ .../database/room-no-op/build.gradle.kts | 68 +++++++---- .../database/room/floconRegisterDatabase.kt | 9 ++ FloconAndroid/database/room/build.gradle.kts | 91 +++++++++------ .../database/room/FloconRoomDatabaseModel.kt | 110 ++++++++++++++++++ .../room}/FloconDatabasePlugin.android.kt | 99 +++++++--------- .../room/FloconSqliteDatabaseModel.kt | 8 ++ .../sample-android-only/build.gradle.kts | 7 ++ .../flocon/myapplication/MainActivity.kt | 14 ++- 13 files changed, 316 insertions(+), 144 deletions(-) delete mode 100644 FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconSqliteDatabaseModel.kt create mode 100644 FloconAndroid/database/core/src/jvmMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.jvm.kt create mode 100644 FloconAndroid/database/room-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt create mode 100644 FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt rename FloconAndroid/database/{core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core => room/src/main/java/io/github/openflocon/flocon/database/room}/FloconDatabasePlugin.android.kt (71%) create mode 100644 FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt diff --git a/FloconAndroid/database/core/build.gradle.kts b/FloconAndroid/database/core/build.gradle.kts index 295d31de3..1d07bb50d 100644 --- a/FloconAndroid/database/core/build.gradle.kts +++ b/FloconAndroid/database/core/build.gradle.kts @@ -32,8 +32,6 @@ kotlin { val androidMain by getting { dependencies { - implementation(libs.androidx.sqlite) - implementation(libs.androidx.sqlite.framework) } } @@ -51,7 +49,6 @@ kotlin { iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) dependencies { - implementation(libs.androidx.sqlite.bundled) } } } diff --git a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconSqliteDatabaseModel.kt b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconSqliteDatabaseModel.kt deleted file mode 100644 index a5001434c..000000000 --- a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconSqliteDatabaseModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.database.core - -import androidx.sqlite.db.SupportSQLiteDatabase -import io.github.openflocon.flocon.database.core.model.FloconAndroidSqlDatabaseModel - -internal class FloconSqliteDatabaseModel( - override val displayName: String, - val database: SupportSQLiteDatabase -) : FloconAndroidSqlDatabaseModel diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt index 296894eae..b773c74f7 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt @@ -1,5 +1,6 @@ package io.github.openflocon.flocon.database.core +import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger @@ -8,10 +9,10 @@ import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse import io.github.openflocon.flocon.database.core.model.fromdevice.DeviceDataBaseDataModel import io.github.openflocon.flocon.database.core.model.todevice.DatabaseQueryMessage -import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.error.pluginNotInitialized import kotlinx.coroutines.flow.MutableStateFlow @@ -36,8 +37,6 @@ internal interface FloconDatabaseDataSource { ): List } -internal expect fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource - object FloconDatabase : FloconPluginFactory { override val name: String = "Database" override val pluginId: String = Protocol.ToDevice.Database.Plugin @@ -65,7 +64,7 @@ internal class FloconDatabasePluginImpl( private val registeredDatabases = MutableStateFlow>(emptyList()) - private val dataSource = buildFloconDatabaseDataSource(context) + private val dataSource: Nothing = TODO() // buildFloconDatabaseDataSource(context) override suspend fun onMessageReceived( method: String, @@ -79,11 +78,17 @@ internal class FloconDatabasePluginImpl( Protocol.ToDevice.Database.Method.Query -> { val queryMessage = DatabaseQueryMessage.fromJson(message = body) ?: return - val result = dataSource.executeSQL( - registeredDatabases = registeredDatabases.value, - databaseName = queryMessage.database, - query = queryMessage.query, - ) + val databaseModel = + registeredDatabases.value.find { it.displayName == queryMessage.database } + if (databaseModel is io.github.openflocon.flocon.database.core.model.FloconSqlDatabaseModel) { + databaseModel.executeSQL(query = queryMessage.query) + } else { +// dataSource.executeSQL( +// registeredDatabases = registeredDatabases.value, +// databaseName = queryMessage.database, +// query = queryMessage.query, +// ) + } try { // sender.send( // plugin = Protocol.FromDevice.Database.Plugin, @@ -105,9 +110,9 @@ internal class FloconDatabasePluginImpl( } private fun sendAllDatabases(sender: FloconMessageSender) { - val databases = dataSource.getAllDataBases( - registeredDatabases = registeredDatabases.value, - ) +// dataSource.getAllDataBases( +// registeredDatabases = registeredDatabases.value, +// ) try { // sender.send( // plugin = Protocol.FromDevice.Database.Plugin, diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt index 7b33e8822..343c1cf38 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt @@ -1,13 +1,11 @@ package io.github.openflocon.flocon.database.core.model -import androidx.sqlite.db.SupportSQLiteDatabase - interface FloconDatabaseModel { val displayName: String } -interface FloconAndroidSqlDatabaseModel : FloconDatabaseModel { - val database: SupportSQLiteDatabase +interface FloconSqlDatabaseModel : FloconDatabaseModel { + suspend fun executeSQL(query: String): io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse } data class FloconFileDatabaseModel( diff --git a/FloconAndroid/database/core/src/jvmMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.jvm.kt b/FloconAndroid/database/core/src/jvmMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.jvm.kt new file mode 100644 index 000000000..e0c25951c --- /dev/null +++ b/FloconAndroid/database/core/src/jvmMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.jvm.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.database.core + +import io.github.openflocon.flocon.FloconContext + +internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/FloconAndroid/database/room-no-op/build.gradle.kts b/FloconAndroid/database/room-no-op/build.gradle.kts index b5a556424..ffd49f7e0 100644 --- a/FloconAndroid/database/room-no-op/build.gradle.kts +++ b/FloconAndroid/database/room-no-op/build.gradle.kts @@ -1,7 +1,51 @@ plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":database:core-no-op")) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } } android { @@ -10,32 +54,12 @@ android { defaultConfig { minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } -} - -dependencies { - implementation(project(":database:core-no-op")) } diff --git a/FloconAndroid/database/room-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt b/FloconAndroid/database/room-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt new file mode 100644 index 000000000..1aefa1497 --- /dev/null +++ b/FloconAndroid/database/room-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.database.room + +fun floconRegisterDatabase(displayName: String, database: Any) { + // no op +} + +fun floconRegisterDatabase(displayName: String, openHelper: Any, dummy: Boolean = true) { + // no op +} diff --git a/FloconAndroid/database/room/build.gradle.kts b/FloconAndroid/database/room/build.gradle.kts index b60b75333..c828ca467 100644 --- a/FloconAndroid/database/room/build.gradle.kts +++ b/FloconAndroid/database/room/build.gradle.kts @@ -1,7 +1,55 @@ plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + api(project(":database:core")) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.sqlite) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.jetbrains.kotlinx.coroutines.android) + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } } android { @@ -10,41 +58,12 @@ android { defaultConfig { minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } -} - -dependencies { - - api(project(":database:core")) - - implementation(libs.androidx.room.runtime) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.jetbrains.kotlinx.coroutines.core) - implementation(libs.jetbrains.kotlinx.coroutines.android) - - testImplementation(libs.junit) } @@ -76,10 +95,12 @@ mavenPublishing { } } developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } } } scm { diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt new file mode 100644 index 000000000..5f7906836 --- /dev/null +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt @@ -0,0 +1,110 @@ +package io.github.openflocon.flocon.database.room + +import androidx.room.RoomDatabase +import androidx.room.Transactor +import androidx.room.exclusiveTransaction +import androidx.room.execSQL +import androidx.room.useReaderConnection +import androidx.sqlite.SQLiteConnection +import androidx.sqlite.execSQL +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.database.core.databasePlugin +import io.github.openflocon.flocon.database.core.model.FloconSqlDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse + +data class FloconRoomDatabaseModel( + override val displayName: String, + val database: RoomDatabase +) : FloconSqlDatabaseModel { + override suspend fun executeSQL(query: String): DatabaseExecuteSqlResponse { + return try { + database.useReaderConnection { connection -> +// val firstWordUpperCase = getFirstWord(query).uppercase() +// when (firstWordUpperCase) { +// "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) +// "INSERT" -> executeInsert(connection, query) +// "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) +// else -> executeRawQuery(connection, query) +// } + TODO() + } + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "error on executeSQL", + originalSql = query, + ) + } + } +} + +private fun executeSelect( + connection: Transactor, + query: String, +): DatabaseExecuteSqlResponse { + return TODO() +// connection.prepare(query).use { statement -> +// val columnNames = mutableListOf() +// val columnCount = statement.getColumnCount() +// for (i in 0 until columnCount) { +// columnNames.add(statement.getColumnName(i)) +// } +// +// val rows = mutableListOf>() +// while (statement.step()) { +// val values = mutableListOf() +// for (i in 0 until columnCount) { +// values.add(if (statement.isNull(i)) null else statement.getText(i)) +// } +// rows.add(values) +// } +// +// DatabaseExecuteSqlResponse.Select( +// columns = columnNames, +// values = rows, +// ) +// } +} + +private fun executeInsert( + connection: SQLiteConnection, + query: String, +): DatabaseExecuteSqlResponse { + connection.execSQL(query) + // SQLite doesn't easily return the last inserted ID via the statement itself without extra queries like last_insert_rowid() + // But for inspection purposes, we might just return 0 or query it. + // For now, let's just return a successful RawSuccess or implement last_insert_rowid + val id = connection.prepare("SELECT last_insert_rowid()").use { it.step(); it.getLong(0) } + return DatabaseExecuteSqlResponse.Insert(id) +} + +private fun executeUpdateDelete( + connection: SQLiteConnection, + query: String, +): DatabaseExecuteSqlResponse { + connection.execSQL(query) + val count = connection.prepare("SELECT changes()").use { it.step(); it.getLong(0).toInt() } + return DatabaseExecuteSqlResponse.UpdateDelete(count) +} + +private fun executeRawQuery( + connection: SQLiteConnection, + query: String, +): DatabaseExecuteSqlResponse { + connection.execSQL(query) + return DatabaseExecuteSqlResponse.RawSuccess +} + +private fun getFirstWord(s: String): String { + val trimmed = s.trim() + val firstSpace = trimmed.indexOf(' ') + return if (firstSpace >= 0) trimmed.substring(0, firstSpace) else trimmed +} + +fun floconRegisterDatabase(displayName: String, database: RoomDatabase) { + Flocon.databasePlugin.register( + FloconRoomDatabaseModel( + displayName = displayName, + database = database, + ) + ) +} diff --git a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.android.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt similarity index 71% rename from FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.android.kt rename to FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt index a35064604..d2a8ee323 100644 --- a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.android.kt +++ b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt @@ -1,21 +1,23 @@ -package io.github.openflocon.flocon.database.core +package io.github.openflocon.flocon.database.room import android.content.Context import android.database.Cursor import android.database.sqlite.SQLiteDatabase -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.database.core.model.FloconAndroidSqlDatabaseModel +import io.github.openflocon.flocon.database.core.FloconDatabaseDataSource import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.FloconSqlDatabaseModel import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse import io.github.openflocon.flocon.database.core.model.fromdevice.DeviceDataBaseDataModel import java.io.File import java.util.Locale -internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { - return FloconDatabaseDataSourceAndroid(context.context) +interface FloconAndroidSqlDatabaseModel : FloconSqlDatabaseModel { + val database: SQLiteDatabase + + override suspend fun executeSQL(query: String): DatabaseExecuteSqlResponse { + return executeSQLInternal(database, query) + } } internal class FloconDatabaseDataSourceAndroid(private val context: Context) : @@ -31,7 +33,7 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : val databaseModel = registeredDatabases.find { it.displayName == databaseName } return when (databaseModel) { is FloconAndroidSqlDatabaseModel -> { - executeSQL( + executeSQLInternal( database = databaseModel.database, query = query, ) @@ -48,31 +50,16 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : databaseName: String, query: String ): DatabaseExecuteSqlResponse { - var helper: SupportSQLiteOpenHelper? = null + var database: SQLiteDatabase? = null return try { val path = context.getDatabasePath(databaseName) - val version = getDatabaseVersion(path = path.absolutePath) - helper = FrameworkSQLiteOpenHelperFactory().create( - SupportSQLiteOpenHelper.Configuration.builder(context) - .name(path.absolutePath) - .callback(object : SupportSQLiteOpenHelper.Callback(version) { - override fun onCreate(db: SupportSQLiteDatabase) { - // no op - } - - override fun onUpgrade( - db: SupportSQLiteDatabase, - oldVersion: Int, - newVersion: Int - ) { - // no op - } - }) - .build() + database = SQLiteDatabase.openDatabase( + path.absolutePath, + null, + SQLiteDatabase.OPEN_READWRITE ) - val database = helper.writableDatabase - executeSQL( + executeSQLInternal( database = database, query = query, ) @@ -82,27 +69,7 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : originalSql = query, ) } finally { - helper?.close() - } - } - - private fun executeSQL( - database: SupportSQLiteDatabase, - query: String - ): DatabaseExecuteSqlResponse { - return try { - val firstWordUpperCase = getFirstWord(query).uppercase(Locale.getDefault()) - when (firstWordUpperCase) { - "UPDATE", "DELETE" -> executeUpdateDelete(database, query) - "INSERT" -> executeInsert(database, query) - "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(database, query) - else -> executeRawQuery(database, query) - } - } catch (t: Throwable) { - DatabaseExecuteSqlResponse.Error( - message = t.message ?: "error on executeSQL", - originalSql = query, - ) + database?.close() } } @@ -126,7 +93,7 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : foundDatabases: MutableList ) { if (depth >= MAX_DEPTH) { - return; + return } directory.listFiles()?.forEach { file -> if (file.isDirectory) { @@ -155,11 +122,31 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : } } +private fun executeSQLInternal( + database: SQLiteDatabase, + query: String +): DatabaseExecuteSqlResponse { + return try { + val firstWordUpperCase = getFirstWord(query).uppercase(Locale.getDefault()) + when (firstWordUpperCase) { + "UPDATE", "DELETE" -> executeUpdateDelete(database, query) + "INSERT" -> executeInsert(database, query) + "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(database, query) + else -> executeRawQuery(database, query) + } + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "error on executeSQL", + originalSql = query, + ) + } +} + private fun executeSelect( - database: SupportSQLiteDatabase, + database: SQLiteDatabase, query: String, ): DatabaseExecuteSqlResponse { - val cursor: Cursor = database.query(query) + val cursor: Cursor = database.rawQuery(query, null) try { val columnNames = cursor.columnNames.toList() val rows = cursorToList(cursor) @@ -173,7 +160,7 @@ private fun executeSelect( } private fun executeUpdateDelete( - database: SupportSQLiteDatabase, + database: SQLiteDatabase, query: String, ): DatabaseExecuteSqlResponse { val statement = database.compileStatement(query) @@ -182,7 +169,7 @@ private fun executeUpdateDelete( } private fun executeInsert( - database: SupportSQLiteDatabase, + database: SQLiteDatabase, query: String, ): DatabaseExecuteSqlResponse { val statement = database.compileStatement(query) @@ -191,7 +178,7 @@ private fun executeInsert( } private fun executeRawQuery( - database: SupportSQLiteDatabase, + database: SQLiteDatabase, query: String, ): DatabaseExecuteSqlResponse { database.execSQL(query) diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt new file mode 100644 index 000000000..d9c57cda1 --- /dev/null +++ b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocon.database.room + +import androidx.sqlite.db.SupportSQLiteDatabase + +internal class FloconSqliteDatabaseModel( + override val displayName: String, + override val database: SupportSQLiteDatabase +) : FloconAndroidSqlDatabaseModel diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index 64ecd4299..e136cdea7 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) alias(libs.plugins.apollo) + id("com.google.protobuf") } @@ -87,11 +88,17 @@ dependencies { debugImplementation(project(":deeplinks")) releaseImplementation(project(":deeplinks-no-op")) + debugImplementation(project(":database:room")) + releaseImplementation(project(":database:room-no-op")) + debugImplementation(project(":network:okhttp-interceptor")) releaseImplementation(project(":network:okhttp-interceptor-no-op")) + implementation(project(":grpc:grpc-interceptor-lite")) + debugImplementation(project(":network:ktor-interceptor")) releaseImplementation(project(":network:ktor-interceptor-no-op")) + debugImplementation(project(":datastores")) releaseImplementation(project(":datastores-no-op")) } diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index 99ef90936..1f8a0a878 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.FloconDatabase +import io.github.openflocon.flocon.database.room.floconRegisterDatabase import io.github.openflocon.flocon.myapplication.database.DogDatabase import io.github.openflocon.flocon.myapplication.database.initializeInMemoryDatabases import io.github.openflocon.flocon.myapplication.database.model.DogEntity @@ -232,10 +234,16 @@ class MainActivity : ComponentActivity() { ) } - install(FloconNetwork) { - - } + install(FloconNetwork) + install(FloconDatabase) } } + private fun toMigrate() { + floconRegisterDatabase( + displayName = "inmemory_dogs", + openHelper = it.openHelper + ) + } + } \ No newline at end of file From 43f1969db74a3d0476aaeb84e448b031a69098d8 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Fri, 13 Mar 2026 14:33:28 +0100 Subject: [PATCH 15/38] feat: Database compile --- .../core/FloconDatabasePluginImpl.android.kt | 281 ++++++++++++++++++ .../flocon/database/core/FloconDatabase.kt | 31 ++ .../database/core/FloconDatabaseConfig.kt | 12 + .../database/core/FloconDatabaseEncoding.kt | 27 ++ .../database/core/FloconDatabasePlugin.kt | 140 +-------- .../database/core/FloconDatabasePluginImpl.kt | 111 +++++++ .../datasource/FloconDatabaseDataSource.kt | 20 ++ .../core/datasource/FloconDatabaseProvider.kt | 14 + .../core/model/FloconDatabaseModel.kt | 13 +- .../fromdevice/DatabaseExecuteResponse.kt | 11 + .../fromdevice/DatabaseExecuteSqlResponse.kt | 17 +- .../{ => sql}/DatabaseQueryLogModel.kt | 3 +- .../{ => sql}/DeviceDataBaseDataModel.kt | 2 +- .../{ => sql}/QueryResultReceivedDataModel.kt | 5 +- .../database/core/FloconDatabasePlugin.ios.kt | 2 +- .../database/room/FloconRoomDatabaseConfig.kt | 24 ++ .../database/room/FloconRoomDatabaseModel.kt | 106 ++++--- .../room/FloconRoomDatabaseProviderImpl.kt | 38 +++ .../room/FloconDatabasePlugin.android.kt | 16 +- .../room/FloconRoomDatabaseModel.android.kt | 25 ++ .../database/room/FloconRoomDatabaseModel.kt | 28 -- .../room/FloconSqliteDatabaseModel.kt | 3 +- .../openflocon/flocon/FloconConfiguration.kt | 18 -- .../openflocon/flocon/FloconEncoding.kt | 18 ++ .../github/openflocon/flocon/FloconPlugin.kt | 3 + .../flocon/myapplication/MainActivity.kt | 16 +- 26 files changed, 731 insertions(+), 253 deletions(-) create mode 100644 FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.android.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabaseConfig.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabaseEncoding.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseProvider.kt create mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteResponse.kt rename FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/{ => sql}/DatabaseQueryLogModel.kt (92%) rename FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/{ => sql}/DeviceDataBaseDataModel.kt (98%) rename FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/{ => sql}/QueryResultReceivedDataModel.kt (74%) create mode 100644 FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseConfig.kt create mode 100644 FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt create mode 100644 FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.android.kt delete mode 100644 FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconEncoding.kt diff --git a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.android.kt b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.android.kt new file mode 100644 index 000000000..f467855ea --- /dev/null +++ b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.android.kt @@ -0,0 +1,281 @@ +package io.github.openflocon.flocon.database.core + +import android.content.Context +import android.database.Cursor +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseDataSource +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel +import java.io.File + +internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { + return FloconDatabaseDataSourceAndroid(context.context) +} + +internal class FloconDatabaseDataSourceAndroid( + private val context: Context +) : FloconDatabaseDataSource { + + override suspend fun executeQuery( + registeredDatabases: List, + databaseName: String, + query: String + ): DatabaseExecuteResponse? { + val databaseModel = registeredDatabases.find { it.displayName == databaseName } + + return databaseModel?.executeQuery( + query = query + ) +// return when(databaseModel) { +// is FloconSqliteDatabaseModel -> { +// executeQuery( +// database = databaseModel.database, +// query = query, +// ) +// } +// else -> openDbAndExecuteQuery( +// databaseName = databaseName, +// query = query, +// ) +// } + } + +// private fun openDbAndExecuteQuery( +// databaseName: String, +// query: String +// ): DatabaseExecuteSqlResponse { +// var helper: SupportSQLiteOpenHelper? = null +// return try { +// val path = context.getDatabasePath(databaseName) +// val version = getDatabaseVersion(path = path.absolutePath) +// helper = FrameworkSQLiteOpenHelperFactory().create( +// SupportSQLiteOpenHelper.Configuration.builder(context) +// .name(path.absolutePath) +// .callback(object : SupportSQLiteOpenHelper.Callback(version) { +// override fun onCreate(db: SupportSQLiteDatabase) { +// // no op +// } +// +// override fun onUpgrade( +// db: SupportSQLiteDatabase, +// oldVersion: Int, +// newVersion: Int +// ) { +// // no op +// } +// }) +// .build() +// ) +// val database = helper.writableDatabase +// +// executeSQL( +// database = database, +// query = query, +// ) +// } catch (t: Throwable) { +// DatabaseExecuteSqlResponse.Error( +// message = t.message ?: "error on executeSQL", +// originalSql = query, +// ) +// } finally { +// helper?.close() +// } +// } + +// private fun executeSQL( +// database: SupportSQLiteDatabase, +// query: String +// ): DatabaseExecuteSqlResponse { +// return try { +// val firstWordUpperCase = getFirstWord(query).uppercase(Locale.getDefault()) +// when (firstWordUpperCase) { +// "UPDATE", "DELETE" -> executeUpdateDelete(database, query) +// "INSERT" -> executeInsert(database, query) +// "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(database, query) +// else -> executeRawQuery(database, query) +// } +// } catch (t: Throwable) { +// DatabaseExecuteSqlResponse.Error( +// message = t.message ?: "error on executeSQL", +// originalSql = query, +// ) +// } +// } + + override fun getAllDataBases( + registeredDatabases: List + ): List { + val databasesDir = context.getDatabasePath("dummy_db").parentFile ?: return emptyList() + + val foundDatabases = mutableListOf() + // Start the recursive search from the base databases directory + scanDirectoryForDatabases( + directory = databasesDir, + depth = 0, + foundDatabases = foundDatabases + ) + + registeredDatabases.forEach { + when (it) { + is FloconFileDatabaseModel -> { + // check if file exists here + if (File(it.absolutePath).exists()) { + foundDatabases.add( + DeviceDataBaseDataModel( + id = it.absolutePath, + name = it.displayName, + ) + ) + } + } + + else -> { + foundDatabases.add( + DeviceDataBaseDataModel( + id = it.displayName, + name = it.displayName, + ) + ) + } + } + } + + return foundDatabases + } + + /** + * Recursively scans a directory for SQLite database files. + * + * @param directory The current directory to scan. + * @param foundDatabases The mutable list to add found databases to. + */ + private fun scanDirectoryForDatabases( + directory: File, + depth: Int, + foundDatabases: MutableList + ) { + if (depth >= MAX_DEPTH) { + return + } + directory.listFiles()?.forEach { file -> + if (file.isDirectory) { + // If it's a directory, recursively call this function + scanDirectoryForDatabases( + directory = file, + depth = depth + 1, + foundDatabases = foundDatabases, + ) + } else { + // If it's a file, check if it's a database file + if (file.isFile && + !file.name.endsWith("-wal") && // Write-Ahead Log + !file.name.endsWith("-shm") && // Shared-Memory + !file.name.endsWith("-journal") // Older journaling mode + ) { + foundDatabases.add( + DeviceDataBaseDataModel( + id = file.absolutePath, // Use absolute path for unique ID + name = file.name, + ) + ) + } + } + } + } + + companion object { + private const val MAX_DEPTH = 7 + } +} + + +//private fun executeSelect( +// database: SupportSQLiteDatabase, +// query: String, +//): DatabaseExecuteSqlResponse { +// val cursor: Cursor = database.query(query) +// try { +// val columnNames = cursor.columnNames.toList() +// val rows = cursorToList(cursor) +// return DatabaseExecuteSqlResponse.Select( +// columns = columnNames, +// values = rows, +// ) +// } finally { +// cursor.close() +// } +//} + +//private fun executeUpdateDelete( +// database: SupportSQLiteDatabase, +// query: String, +//): DatabaseExecuteSqlResponse { +// val statement = database.compileStatement(query) +// val count: Int = statement.executeUpdateDelete() +// return DatabaseExecuteSqlResponse.UpdateDelete(count) +//} +// +//private fun executeInsert( +// database: SupportSQLiteDatabase, +// query: String, +//): DatabaseExecuteSqlResponse { +// val statement = database.compileStatement(query) +// val insertedId: Long = statement.executeInsert() +// return DatabaseExecuteSqlResponse.Insert(insertedId) +//} +// +//private fun executeRawQuery( +// database: SupportSQLiteDatabase, +// query: String, +//): DatabaseExecuteSqlResponse { +// database.execSQL(query) +// return DatabaseExecuteSqlResponse.RawSuccess +//} + +private fun getFirstWord(s: String): String { + var s = s + s = s.trim { it <= ' ' } + val firstSpace = s.indexOf(' ') + return if (firstSpace >= 0) s.substring(0, firstSpace) else s +} + +private fun cursorToList(cursor: Cursor): List> { + val rows = mutableListOf>() + val numColumns = cursor.columnCount + while (cursor.moveToNext()) { + val values = mutableListOf() + for (column in 0.. null + Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(column).toString() + Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(column).toString() + Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(column).toString() + Cursor.FIELD_TYPE_STRING -> cursor.getString(column).toString() + else -> cursor.getString(column) + } +} + +// must use the old way to get the version... +private fun getDatabaseVersion( + path: String, +): Int { + return android.database.sqlite.SQLiteDatabase.openDatabase( + path, + null, + android.database.sqlite.SQLiteDatabase.OPEN_READONLY + ).use { db -> + db.rawQuery("PRAGMA user_version", null).use { cursor -> + if (cursor.moveToFirst()) cursor.getInt(0) else 0 + } + } +} \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt new file mode 100644 index 000000000..219abe66d --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt @@ -0,0 +1,31 @@ +package io.github.openflocon.flocon.database.core + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconEncoding +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.dsl.FloconMarker + +object FloconDatabase : FloconPluginFactory { + override val name: String = "Database" + override val pluginId: String = Protocol.ToDevice.Database.Plugin + + @FloconMarker + override fun createEncoding(): FloconEncoding = FloconDatabaseEncoding() + + override fun createConfig() = FloconDatabaseConfig() + + @OptIn(FloconMarker::class) + override fun install( + pluginConfig: FloconDatabaseConfig, + floconConfig: FloconConfig + ): FloconDatabasePlugin { + return FloconDatabasePluginImpl( + sender = floconConfig.client as FloconMessageSender, + context = floconConfig.context, + providers = pluginConfig.providers + ).also { FloconDatabasePluginImpl.plugin = it } + } + +} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabaseConfig.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabaseConfig.kt new file mode 100644 index 000000000..b3dbc5b75 --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabaseConfig.kt @@ -0,0 +1,12 @@ +package io.github.openflocon.flocon.database.core + +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseProvider +import io.github.openflocon.flocon.dsl.FloconMarker + +class FloconDatabaseConfig : FloconPluginConfig { + + @FloconMarker + val providers: MutableList = mutableListOf() + +} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabaseEncoding.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabaseEncoding.kt new file mode 100644 index 000000000..2a1b5cf6b --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabaseEncoding.kt @@ -0,0 +1,27 @@ +package io.github.openflocon.flocon.database.core + +import io.github.openflocon.flocon.FloconEncoding +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.dsl.FloconMarker +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass + +@FloconMarker +internal class FloconDatabaseEncoding : FloconEncoding { + override val serializersModule: SerializersModule + get() = SerializersModule { + polymorphic(DatabaseExecuteResponse::class) { + polymorphic(DatabaseExecuteSqlResponse::class) { + subclass(DatabaseExecuteSqlResponse.Insert::class) + subclass(DatabaseExecuteSqlResponse.UpdateDelete::class) + subclass(DatabaseExecuteSqlResponse.Select::class) + subclass(DatabaseExecuteSqlResponse.RawSuccess::class) + subclass(DatabaseExecuteSqlResponse.Error::class) + + defaultDeserializer { DatabaseExecuteSqlResponse.Error.serializer() } + } + } + } +} \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt index b773c74f7..704879c5b 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.kt @@ -1,151 +1,23 @@ package io.github.openflocon.flocon.database.core import io.github.openflocon.flocon.Flocon -import io.github.openflocon.flocon.FloconConfig -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.FloconPluginFactory -import io.github.openflocon.flocon.Protocol -import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseProvider import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel -import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.DeviceDataBaseDataModel -import io.github.openflocon.flocon.database.core.model.todevice.DatabaseQueryMessage import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.error.pluginNotInitialized -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update - -class FloconDatabaseConfig : FloconPluginConfig interface FloconDatabasePlugin : FloconPlugin { - fun register(floconDatabaseModel: FloconDatabaseModel) - fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) -} - -internal interface FloconDatabaseDataSource { - fun executeSQL( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteSqlResponse - - fun getAllDataBases( - registeredDatabases: List - ): List -} - -object FloconDatabase : FloconPluginFactory { - override val name: String = "Database" - override val pluginId: String = Protocol.ToDevice.Database.Plugin - override fun createConfig() = FloconDatabaseConfig() - override fun install( - pluginConfig: FloconDatabaseConfig, - floconConfig: FloconConfig - ): FloconDatabasePlugin { - return FloconDatabasePluginImpl( - sender = floconConfig.client as FloconMessageSender, - context = floconConfig.context - ).also { FloconDatabasePluginImpl.plugin = it } - } -} -internal class FloconDatabasePluginImpl( - private var sender: FloconMessageSender, - private val context: FloconContext, -) : FloconPlugin, FloconDatabasePlugin { - override val key: String = "DATABASE" + @FloconMarker + val providers: List - companion object { - var plugin: FloconDatabasePlugin? = null - } - - private val registeredDatabases = MutableStateFlow>(emptyList()) - - private val dataSource: Nothing = TODO() // buildFloconDatabaseDataSource(context) - - override suspend fun onMessageReceived( - method: String, - body: String, - ) { - when (method) { - Protocol.ToDevice.Database.Method.GetDatabases -> { - sendAllDatabases(sender) - } - - Protocol.ToDevice.Database.Method.Query -> { - val queryMessage = - DatabaseQueryMessage.fromJson(message = body) ?: return - val databaseModel = - registeredDatabases.value.find { it.displayName == queryMessage.database } - if (databaseModel is io.github.openflocon.flocon.database.core.model.FloconSqlDatabaseModel) { - databaseModel.executeSQL(query = queryMessage.query) - } else { -// dataSource.executeSQL( -// registeredDatabases = registeredDatabases.value, -// databaseName = queryMessage.database, -// query = queryMessage.query, -// ) - } - try { -// sender.send( -// plugin = Protocol.FromDevice.Database.Plugin, -// method = Protocol.FromDevice.Database.Method.Query, -// body = QueryResultDataModel( -// requestId = queryMessage.requestId, -// result = result.toJson(), -// ).toJson(), -// ) - } catch (t: Throwable) { - FloconLogger.logError("Database parsing error", t) - } - } - } - } - - override suspend fun onConnectedToServer() { - sendAllDatabases(sender) - } - - private fun sendAllDatabases(sender: FloconMessageSender) { -// dataSource.getAllDataBases( -// registeredDatabases = registeredDatabases.value, -// ) - try { -// sender.send( -// plugin = Protocol.FromDevice.Database.Plugin, -// method = Protocol.FromDevice.Database.Method.GetDatabases, -// body = listDeviceDataBaseDataModelToJson(databases), -// ) - } catch (t: Throwable) { - FloconLogger.logError("Database parsing error", t) - } - } + fun register(floconDatabaseModel: FloconDatabaseModel) - override fun register(floconDatabaseModel: FloconDatabaseModel) { - registeredDatabases.update { it + floconDatabaseModel } - } + fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) - override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) { - try { -// sender.send( -// plugin = Protocol.FromDevice.Database.Plugin, -// method = Protocol.FromDevice.Database.Method.LogQuery, -// body = DatabaseQueryLogModel( -// dbName = dbName, -// sqlQuery = sqlQuery, -// bindArgs = bindArgs.map { it.toString() }, -// timestamp = currentTimeMillis(), -// ).toJson(), -// ) - } catch (t: Throwable) { - FloconLogger.logError("Database logging error", t) - } - } } @OptIn(FloconMarker::class) val Flocon.Companion.databasePlugin: FloconDatabasePlugin - get() = FloconDatabasePluginImpl.plugin ?: pluginNotInitialized("Database") + get() = FloconDatabasePluginImpl.plugin ?: pluginNotInitialized("Database") \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.kt new file mode 100644 index 000000000..dc07b079b --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.kt @@ -0,0 +1,111 @@ +package io.github.openflocon.flocon.database.core + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseDataSource +import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseProvider +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.QueryResultDataModel +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.listDeviceDataBaseDataModelToJson +import io.github.openflocon.flocon.database.core.model.todevice.DatabaseQueryMessage +import io.github.openflocon.flocon.dsl.FloconMarker +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +internal expect fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource + +internal class FloconDatabasePluginImpl( + private var sender: FloconMessageSender, + private val context: FloconContext, + @FloconMarker + override val providers: List +) : FloconPlugin, FloconDatabasePlugin { + + override val key: String = "DATABASE" + + private val registeredDatabases = MutableStateFlow>(emptyList()) + + private val dataSource = buildFloconDatabaseDataSource(context) + + override suspend fun onMessageReceived( + method: String, + body: String + ) { + when (method) { + Protocol.ToDevice.Database.Method.GetDatabases -> sendAllDatabases(sender) + + Protocol.ToDevice.Database.Method.Query -> { + val queryMessage = DatabaseQueryMessage.fromJson(message = body) ?: return + val databaseModel = registeredDatabases.value + .find { it.displayName == queryMessage.database } + val result = databaseModel?.executeQuery(query = queryMessage.query) + ?: DatabaseExecuteSqlResponse.Error( + message = "Database not found", + originalSql = queryMessage.query, + ) + + try { + sender.send( + plugin = Protocol.FromDevice.Database.Plugin, + method = Protocol.FromDevice.Database.Method.Query, + body = QueryResultDataModel( + requestId = queryMessage.requestId, + result = result + ) + .toJson() + ) + } catch (t: Throwable) { + FloconLogger.logError("Database parsing error", t) + } + } + } + } + + override suspend fun onConnectedToServer() { + sendAllDatabases(sender) + } + + private suspend fun sendAllDatabases(sender: FloconMessageSender) { + val databases = dataSource.getAllDataBases( + registeredDatabases = registeredDatabases.value, + ) + try { + sender.send( + plugin = Protocol.FromDevice.Database.Plugin, + method = Protocol.FromDevice.Database.Method.GetDatabases, + body = listDeviceDataBaseDataModelToJson(databases), + ) + } catch (t: Throwable) { + FloconLogger.logError("Database parsing error", t) + } + } + + override fun register(floconDatabaseModel: FloconDatabaseModel) { + registeredDatabases.update { it + floconDatabaseModel } + } + + override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) { + try { +// sender.send( +// plugin = Protocol.FromDevice.Database.Plugin, +// method = Protocol.FromDevice.Database.Method.LogQuery, +// body = DatabaseQueryLogModel( +// dbName = dbName, +// sqlQuery = sqlQuery, +// bindArgs = bindArgs.map { it.toString() }, +// timestamp = currentTimeMillis(), +// ).toJson(), +// ) + } catch (t: Throwable) { + FloconLogger.logError("Database logging error", t) + } + } + + companion object { + var plugin: FloconDatabasePlugin? = null + } +} \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt new file mode 100644 index 000000000..8415d9e79 --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.flocon.database.core.datasource + +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel + +interface FloconDatabaseDataSource { + + suspend fun executeQuery( + registeredDatabases: List, + databaseName: String, + query: String + ): DatabaseExecuteResponse? + + fun getAllDataBases( + registeredDatabases: List + ): List + +} \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseProvider.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseProvider.kt new file mode 100644 index 000000000..b54fb9d56 --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseProvider.kt @@ -0,0 +1,14 @@ +package io.github.openflocon.flocon.database.core.datasource + +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel +import io.github.openflocon.flocon.dsl.FloconMarker + +interface FloconDatabaseProvider { + + @FloconMarker + fun getAllDataBases( + registeredDatabases: List + ): List + +} \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt index 343c1cf38..8b7899c70 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt @@ -1,14 +1,19 @@ package io.github.openflocon.flocon.database.core.model +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse + interface FloconDatabaseModel { val displayName: String -} -interface FloconSqlDatabaseModel : FloconDatabaseModel { - suspend fun executeSQL(query: String): io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse + suspend fun executeQuery(query: String): DatabaseExecuteResponse + } data class FloconFileDatabaseModel( override val displayName: String, val absolutePath: String -) : FloconDatabaseModel +) : FloconDatabaseModel { + override suspend fun executeQuery(query: String): DatabaseExecuteResponse { + TODO("Not yet implemented") + } +} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteResponse.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteResponse.kt new file mode 100644 index 000000000..03ffb0d12 --- /dev/null +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteResponse.kt @@ -0,0 +1,11 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.github.openflocon.flocon.database.core.model.fromdevice + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator + +@Serializable +@JsonClassDiscriminator("type") +sealed interface DatabaseExecuteResponse \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt index 71a71e9d9..9539a01a0 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt @@ -1,15 +1,17 @@ package io.github.openflocon.flocon.database.core.model.fromdevice import io.github.openflocon.flocon.core.FloconEncoder +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.put @Serializable -sealed interface DatabaseExecuteSqlResponse { +sealed interface DatabaseExecuteSqlResponse : DatabaseExecuteResponse { @Serializable + @SerialName("SELECT") // Case for successful SELECT queries class Select( val columns: List, @@ -18,25 +20,29 @@ sealed interface DatabaseExecuteSqlResponse { // Case for successful INSERT queries @Serializable + @SerialName("INSERT") class Insert( val insertedId: Long ) : DatabaseExecuteSqlResponse // Case for successful UPDATE or DELETE queries @Serializable + @SerialName("UPDATE_DELETE") class UpdateDelete( val affectedCount: Int ) : DatabaseExecuteSqlResponse // Case for successful "raw" queries (CREATE TABLE, DROP TABLE, etc.) @Serializable + @SerialName("RAW_SUCCESS") object RawSuccess : DatabaseExecuteSqlResponse // Case for an SQL execution error @Serializable + @SerialName("ERROR") class Error( - val message: String, // Detailed error message - val originalSql: String, // SQL query that caused the error (optional) + val message: String = "", // Detailed error message + val originalSql: String = "", // SQL query that caused the error (optional) ) : DatabaseExecuteSqlResponse } @@ -54,6 +60,9 @@ fun DatabaseExecuteSqlResponse.toJson(): String { return buildJsonObject { put("type", type) - put("body", thisAsJson.toString()) // warning : the desktop is waiting for a string representation of the json here + put( + "body", + thisAsJson.toString() + ) // warning : the desktop is waiting for a string representation of the json here }.toString() } diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseQueryLogModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DatabaseQueryLogModel.kt similarity index 92% rename from FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseQueryLogModel.kt rename to FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DatabaseQueryLogModel.kt index 74359152f..dbb7a4507 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseQueryLogModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DatabaseQueryLogModel.kt @@ -1,7 +1,8 @@ -package io.github.openflocon.flocon.database.core.model.fromdevice +package io.github.openflocon.flocon.database.core.model.fromdevice.sql import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @Serializable diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DeviceDataBaseDataModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt similarity index 98% rename from FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DeviceDataBaseDataModel.kt rename to FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt index 42e00113b..337bb00f4 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DeviceDataBaseDataModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.database.core.model.fromdevice +package io.github.openflocon.flocon.database.core.model.fromdevice.sql import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/QueryResultReceivedDataModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt similarity index 74% rename from FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/QueryResultReceivedDataModel.kt rename to FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt index c4e51d071..1d1da7d6c 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/QueryResultReceivedDataModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt @@ -1,13 +1,14 @@ -package io.github.openflocon.flocon.database.core.model.fromdevice +package io.github.openflocon.flocon.database.core.model.fromdevice.sql import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString @Serializable internal data class QueryResultDataModel( val requestId: String, - val result: String, + val result: DatabaseExecuteResponse, ) { fun toJson(): String { return FloconEncoder.json.encodeToString(this) diff --git a/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt b/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt index f0645b467..b6b4286b7 100644 --- a/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt +++ b/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt @@ -6,7 +6,7 @@ import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.DeviceDataBaseDataModel +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel import platform.Foundation.NSFileManager internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseConfig.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseConfig.kt new file mode 100644 index 000000000..40d7007f1 --- /dev/null +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseConfig.kt @@ -0,0 +1,24 @@ +package io.github.openflocon.flocon.database.room + +import io.github.openflocon.flocon.database.core.FloconDatabaseConfig +import io.github.openflocon.flocon.dsl.FloconMarker + +class FloconRoomDatabaseConfig internal constructor() { + internal val paths: MutableList = mutableListOf() + + fun path(path: String) { + paths.add(path) + } + +} + +@OptIn(FloconMarker::class) +fun FloconDatabaseConfig.room(block: FloconRoomDatabaseConfig.() -> Unit = {}) { + val config = FloconRoomDatabaseConfig().apply(block) + + providers.add( + FloconRoomDatabaseProviderImpl( + paths = config.paths + ) + ) +} \ No newline at end of file diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt index 5f7906836..c97d82cd8 100644 --- a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt @@ -2,31 +2,48 @@ package io.github.openflocon.flocon.database.room import androidx.room.RoomDatabase import androidx.room.Transactor -import androidx.room.exclusiveTransaction import androidx.room.execSQL import androidx.room.useReaderConnection -import androidx.sqlite.SQLiteConnection -import androidx.sqlite.execSQL import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.database.core.databasePlugin -import io.github.openflocon.flocon.database.core.model.FloconSqlDatabaseModel +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse data class FloconRoomDatabaseModel( override val displayName: String, val database: RoomDatabase -) : FloconSqlDatabaseModel { - override suspend fun executeSQL(query: String): DatabaseExecuteSqlResponse { +) : FloconDatabaseModel { + + override suspend fun executeQuery(query: String): DatabaseExecuteResponse { return try { database.useReaderConnection { connection -> -// val firstWordUpperCase = getFirstWord(query).uppercase() -// when (firstWordUpperCase) { -// "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) -// "INSERT" -> executeInsert(connection, query) -// "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) -// else -> executeRawQuery(connection, query) -// } - TODO() + val firstWordUpperCase = getFirstWord(query).uppercase() + + when (firstWordUpperCase) { + "SELECT", + "PRAGMA", + "EXPLAIN" -> executeSelect( + connection = connection, + query = query + ) + + "INSERT" -> executeInsert( + connection = connection, + query = query + ) + + "UPDATE", + "DELETE" -> executeUpdateDelete( + connection = connection, + query = query + ) + + else -> executeRawQuery( + connection = connection, + query = query + ) + } } } catch (t: Throwable) { DatabaseExecuteSqlResponse.Error( @@ -37,57 +54,56 @@ data class FloconRoomDatabaseModel( } } -private fun executeSelect( +private suspend fun executeSelect( connection: Transactor, query: String, ): DatabaseExecuteSqlResponse { - return TODO() -// connection.prepare(query).use { statement -> -// val columnNames = mutableListOf() -// val columnCount = statement.getColumnCount() -// for (i in 0 until columnCount) { -// columnNames.add(statement.getColumnName(i)) -// } -// -// val rows = mutableListOf>() -// while (statement.step()) { -// val values = mutableListOf() -// for (i in 0 until columnCount) { -// values.add(if (statement.isNull(i)) null else statement.getText(i)) -// } -// rows.add(values) -// } -// -// DatabaseExecuteSqlResponse.Select( -// columns = columnNames, -// values = rows, -// ) -// } + return connection.usePrepared(query) { statement -> + val columnNames = mutableListOf() + val columnCount = statement.getColumnCount() + for (i in 0 until columnCount) { + columnNames.add(statement.getColumnName(i)) + } + + val rows = mutableListOf>() + while (statement.step()) { + val values = mutableListOf() + for (i in 0 until columnCount) { + values.add(if (statement.isNull(i)) null else statement.getText(i)) + } + rows.add(values) + } + + DatabaseExecuteSqlResponse.Select( + columns = columnNames, + values = rows + ) + } } -private fun executeInsert( - connection: SQLiteConnection, +private suspend fun executeInsert( + connection: Transactor, query: String, ): DatabaseExecuteSqlResponse { connection.execSQL(query) // SQLite doesn't easily return the last inserted ID via the statement itself without extra queries like last_insert_rowid() // But for inspection purposes, we might just return 0 or query it. // For now, let's just return a successful RawSuccess or implement last_insert_rowid - val id = connection.prepare("SELECT last_insert_rowid()").use { it.step(); it.getLong(0) } + val id = connection.usePrepared("SELECT last_insert_rowid()") { it.step(); it.getLong(0) } return DatabaseExecuteSqlResponse.Insert(id) } -private fun executeUpdateDelete( - connection: SQLiteConnection, +private suspend fun executeUpdateDelete( + connection: Transactor, query: String, ): DatabaseExecuteSqlResponse { connection.execSQL(query) - val count = connection.prepare("SELECT changes()").use { it.step(); it.getLong(0).toInt() } + val count = connection.usePrepared("SELECT changes()") { it.step(); it.getLong(0).toInt() } return DatabaseExecuteSqlResponse.UpdateDelete(count) } -private fun executeRawQuery( - connection: SQLiteConnection, +private suspend fun executeRawQuery( + connection: Transactor, query: String, ): DatabaseExecuteSqlResponse { connection.execSQL(query) diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt new file mode 100644 index 000000000..db6fc35b3 --- /dev/null +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt @@ -0,0 +1,38 @@ +package io.github.openflocon.flocon.database.room + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.database.core.databasePlugin +import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseProvider +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel +import io.github.openflocon.flocon.dsl.FloconMarker + +interface FloconRoomDatabaseProvider : FloconDatabaseProvider { + + // TODO + fun register() + +} + +@OptIn(FloconMarker::class) +internal class FloconRoomDatabaseProviderImpl( + paths: List +) : FloconRoomDatabaseProvider { + + override fun getAllDataBases( + registeredDatabases: List + ): List { + TODO("Not yet implemented") + } + + override fun register() { + TODO("Not yet implemented") + } + +} + +@OptIn(FloconMarker::class) +val Flocon.Companion.databaseRoom: FloconRoomDatabaseProvider + get() = databasePlugin.providers + .firstNotNullOfOrNull { it as? FloconRoomDatabaseProvider } + ?: error("Room database provider not initialized") \ No newline at end of file diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt index d2a8ee323..fce42a575 100644 --- a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt +++ b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt @@ -3,19 +3,19 @@ package io.github.openflocon.flocon.database.room import android.content.Context import android.database.Cursor import android.database.sqlite.SQLiteDatabase -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.database.core.FloconDatabaseDataSource +import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseDataSource import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel -import io.github.openflocon.flocon.database.core.model.FloconSqlDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.DeviceDataBaseDataModel +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel import java.io.File import java.util.Locale -interface FloconAndroidSqlDatabaseModel : FloconSqlDatabaseModel { +interface FloconAndroidSqlDatabaseModel : FloconDatabaseModel { val database: SQLiteDatabase - override suspend fun executeSQL(query: String): DatabaseExecuteSqlResponse { + + override suspend fun executeQuery(query: String): DatabaseExecuteResponse { return executeSQLInternal(database, query) } } @@ -25,11 +25,11 @@ internal class FloconDatabaseDataSourceAndroid(private val context: Context) : private val MAX_DEPTH = 7 - override fun executeSQL( + override suspend fun executeQuery( registeredDatabases: List, databaseName: String, query: String - ): DatabaseExecuteSqlResponse { + ): DatabaseExecuteResponse? { val databaseModel = registeredDatabases.find { it.displayName == databaseName } return when (databaseModel) { is FloconAndroidSqlDatabaseModel -> { diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.android.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.android.kt new file mode 100644 index 000000000..8f40c0b14 --- /dev/null +++ b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.android.kt @@ -0,0 +1,25 @@ +package io.github.openflocon.flocon.database.room + +import android.database.sqlite.SQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper + +//data class FloconRoomDatabaseModel( +// override val displayName: String, +// override val database: SQLiteDatabase +//) : FloconAndroidSqlDatabaseModel + +fun floconRegisterDatabase(displayName: String, database: SQLiteDatabase) { +// Flocon.databasePlugin.register( +// FloconRoomDatabaseModel( +// displayName = displayName, +// database = database +// ) +// ) +} + +fun floconRegisterDatabase(displayName: String, openHelper: SupportSQLiteOpenHelper) { +// floconRegisterDatabase( +// displayName = displayName, +// database = openHelper.writableDatabase., +// ) +} diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt deleted file mode 100644 index b826dda27..000000000 --- a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.openflocon.flocon.database.room - -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import io.github.openflocon.flocon.Flocon -import io.github.openflocon.flocon.database.core.databasePlugin -import io.github.openflocon.flocon.database.core.model.FloconAndroidSqlDatabaseModel - -data class FloconRoomDatabaseModel( - override val displayName: String, - val database: SupportSQLiteDatabase -) : FloconAndroidSqlDatabaseModel - -fun floconRegisterDatabase(displayName: String, database: SupportSQLiteDatabase) { - Flocon.databasePlugin.register( - FloconRoomDatabaseModel( - displayName = displayName, - database = database, - ) - ) -} - -fun floconRegisterDatabase(displayName: String, openHelper: SupportSQLiteOpenHelper) { - floconRegisterDatabase( - displayName = displayName, - database = openHelper.writableDatabase, - ) -} diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt index d9c57cda1..045b20f29 100644 --- a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt +++ b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt @@ -1,8 +1,9 @@ package io.github.openflocon.flocon.database.room +import android.database.sqlite.SQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase internal class FloconSqliteDatabaseModel( override val displayName: String, - override val database: SupportSQLiteDatabase + override val database: SQLiteDatabase//SupportSQLiteDatabase ) : FloconAndroidSqlDatabaseModel diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index fb8f00ce3..5d5d0fda1 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -1,7 +1,6 @@ package io.github.openflocon.flocon import io.github.openflocon.flocon.client.FloconClient -import io.github.openflocon.flocon.dsl.FloconMarker import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -60,21 +59,4 @@ fun startFlocon(context: FloconContext, block: FloconConfiguration.() -> Unit) { config = config, plugins = configuration.build() ) -} - -class DumpObject( - context: FloconContext, - client: Client -) : FloconApp() { - - override val client: Client = client - - private val _initialized = MutableStateFlow(false) - override val isInitialized: StateFlow = _initialized.asStateFlow() - - init { - this.context = context - instance = this - } - } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconEncoding.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconEncoding.kt new file mode 100644 index 000000000..a5f3c0dfe --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconEncoding.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocon + +import io.github.openflocon.flocon.dsl.FloconMarker +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +@FloconMarker +interface FloconEncoding { + + val serializersModule: SerializersModule + +} + +@FloconMarker +internal class DefaultEncoding() : FloconEncoding { + override val serializersModule: SerializersModule + get() = EmptySerializersModule() +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt index e91903615..377e3f4d9 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -34,6 +34,9 @@ interface FloconPluginKey { interface FloconPluginFactory : FloconPluginKey { + @FloconMarker + fun createEncoding(): FloconEncoding = DefaultEncoding() + /** * Create a default configuration instance for the plugin. */ diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index 1f8a0a878..345afea84 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -23,7 +23,9 @@ import androidx.compose.ui.unit.dp import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.database.core.FloconDatabase import io.github.openflocon.flocon.database.room.floconRegisterDatabase +import io.github.openflocon.flocon.database.room.room import io.github.openflocon.flocon.myapplication.database.DogDatabase +import io.github.openflocon.flocon.myapplication.database.initializeDatabases import io.github.openflocon.flocon.myapplication.database.initializeInMemoryDatabases import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.grpc.GrpcController @@ -68,7 +70,7 @@ class MainActivity : ComponentActivity() { .build() // initializeSharedPreferences(applicationContext) -// initializeDatabases(context = applicationContext) + initializeDatabases(context = applicationContext) // FloconLogger.enabled = true // Flocon.initialize(this) @@ -235,15 +237,17 @@ class MainActivity : ComponentActivity() { } install(FloconNetwork) - install(FloconDatabase) + install(FloconDatabase) { + room() + } } } private fun toMigrate() { - floconRegisterDatabase( - displayName = "inmemory_dogs", - openHelper = it.openHelper - ) +// floconRegisterDatabase( +// displayName = "inmemory_dogs", +// openHelper = it.openHelper +// ) } } \ No newline at end of file From 91fb3f862383ed6a0dccb845c39de6e764b92b00 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Fri, 13 Mar 2026 17:01:36 +0100 Subject: [PATCH 16/38] feat: Clean a bit --- .../core/FloconDatabasePluginImpl.android.kt | 281 ------------------ .../core/model/FloconDatabaseModel.android.kt | 46 +++ .../flocon/database/core/FloconDatabase.kt | 9 +- .../database/core/FloconDatabaseConfig.kt | 5 +- .../database/core/FloconDatabasePluginImpl.kt | 63 ++-- .../core/datasource/FloconDatabaseProvider.kt | 3 +- .../core/model/FloconDatabaseModel.kt | 12 +- .../fromdevice/sql/DeviceDataBaseDataModel.kt | 2 +- .../sql/QueryResultReceivedDataModel.kt | 2 +- FloconAndroid/database/room/build.gradle.kts | 3 +- .../FloconRoomDatabaseProviderImpl.android.kt | 75 +++++ .../database/room/FloconRoomDatabaseConfig.kt | 1 + .../database/room/FloconRoomDatabaseModel.kt | 32 +- .../room/FloconRoomDatabaseProviderImpl.kt | 18 +- .../room/FloconRoomDatabaseModel.android.kt | 25 -- .../room/FloconSqliteDatabaseModel.kt | 9 - .../room/extensions/RoomBuilderExt.kt | 25 ++ .../plugins/deeplinks/FloconDeeplinks.kt | 3 +- .../io/github/openflocon/flocon/Flocon.kt | 1 + .../openflocon/flocon/FloconConfiguration.kt | 5 +- .../github/openflocon/flocon/FloconPlugin.kt | 2 +- .../analytics/FloconAnalyticsPlugin.kt | 5 +- .../FloconCrashReporterPlugin.kt | 3 +- .../dashboard/FloconDashboardPlugin.kt | 3 +- .../plugins/device/FloconDevicePluginImpl.kt | 2 +- .../flocon/plugins/files/FloconFilesPlugin.kt | 2 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 2 +- .../plugins/tables/FloconTablesPlugin.kt | 3 +- .../analytics/FloconAnalyticsPlugin.kt | 3 +- .../pluginsold/device/FloconDevicePlugin.kt | 2 +- .../pluginsold/files/FloconFilesPlugin.kt | 3 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 3 +- .../pluginsold/tables/FloconTablesPlugin.kt | 3 +- FloconAndroid/gradle/libs.versions.toml | 15 +- .../flocon/network/core/FloconNetwork.kt | 3 +- .../flocon/myapplication/MainActivity.kt | 11 +- .../myapplication/database/DogDatabase.kt | 24 +- .../database/InitializeDatabases.kt | 16 +- .../sample-multiplatform/build.gradle.kts | 2 +- .../features/database/DatabaseTabViewModel.kt | 2 +- 40 files changed, 296 insertions(+), 433 deletions(-) delete mode 100644 FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.android.kt create mode 100644 FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.android.kt create mode 100644 FloconAndroid/database/room/src/androidMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.android.kt delete mode 100644 FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.android.kt delete mode 100644 FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt create mode 100644 FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/extensions/RoomBuilderExt.kt diff --git a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.android.kt b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.android.kt deleted file mode 100644 index f467855ea..000000000 --- a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.android.kt +++ /dev/null @@ -1,281 +0,0 @@ -package io.github.openflocon.flocon.database.core - -import android.content.Context -import android.database.Cursor -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseDataSource -import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel -import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel -import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel -import java.io.File - -internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { - return FloconDatabaseDataSourceAndroid(context.context) -} - -internal class FloconDatabaseDataSourceAndroid( - private val context: Context -) : FloconDatabaseDataSource { - - override suspend fun executeQuery( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteResponse? { - val databaseModel = registeredDatabases.find { it.displayName == databaseName } - - return databaseModel?.executeQuery( - query = query - ) -// return when(databaseModel) { -// is FloconSqliteDatabaseModel -> { -// executeQuery( -// database = databaseModel.database, -// query = query, -// ) -// } -// else -> openDbAndExecuteQuery( -// databaseName = databaseName, -// query = query, -// ) -// } - } - -// private fun openDbAndExecuteQuery( -// databaseName: String, -// query: String -// ): DatabaseExecuteSqlResponse { -// var helper: SupportSQLiteOpenHelper? = null -// return try { -// val path = context.getDatabasePath(databaseName) -// val version = getDatabaseVersion(path = path.absolutePath) -// helper = FrameworkSQLiteOpenHelperFactory().create( -// SupportSQLiteOpenHelper.Configuration.builder(context) -// .name(path.absolutePath) -// .callback(object : SupportSQLiteOpenHelper.Callback(version) { -// override fun onCreate(db: SupportSQLiteDatabase) { -// // no op -// } -// -// override fun onUpgrade( -// db: SupportSQLiteDatabase, -// oldVersion: Int, -// newVersion: Int -// ) { -// // no op -// } -// }) -// .build() -// ) -// val database = helper.writableDatabase -// -// executeSQL( -// database = database, -// query = query, -// ) -// } catch (t: Throwable) { -// DatabaseExecuteSqlResponse.Error( -// message = t.message ?: "error on executeSQL", -// originalSql = query, -// ) -// } finally { -// helper?.close() -// } -// } - -// private fun executeSQL( -// database: SupportSQLiteDatabase, -// query: String -// ): DatabaseExecuteSqlResponse { -// return try { -// val firstWordUpperCase = getFirstWord(query).uppercase(Locale.getDefault()) -// when (firstWordUpperCase) { -// "UPDATE", "DELETE" -> executeUpdateDelete(database, query) -// "INSERT" -> executeInsert(database, query) -// "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(database, query) -// else -> executeRawQuery(database, query) -// } -// } catch (t: Throwable) { -// DatabaseExecuteSqlResponse.Error( -// message = t.message ?: "error on executeSQL", -// originalSql = query, -// ) -// } -// } - - override fun getAllDataBases( - registeredDatabases: List - ): List { - val databasesDir = context.getDatabasePath("dummy_db").parentFile ?: return emptyList() - - val foundDatabases = mutableListOf() - // Start the recursive search from the base databases directory - scanDirectoryForDatabases( - directory = databasesDir, - depth = 0, - foundDatabases = foundDatabases - ) - - registeredDatabases.forEach { - when (it) { - is FloconFileDatabaseModel -> { - // check if file exists here - if (File(it.absolutePath).exists()) { - foundDatabases.add( - DeviceDataBaseDataModel( - id = it.absolutePath, - name = it.displayName, - ) - ) - } - } - - else -> { - foundDatabases.add( - DeviceDataBaseDataModel( - id = it.displayName, - name = it.displayName, - ) - ) - } - } - } - - return foundDatabases - } - - /** - * Recursively scans a directory for SQLite database files. - * - * @param directory The current directory to scan. - * @param foundDatabases The mutable list to add found databases to. - */ - private fun scanDirectoryForDatabases( - directory: File, - depth: Int, - foundDatabases: MutableList - ) { - if (depth >= MAX_DEPTH) { - return - } - directory.listFiles()?.forEach { file -> - if (file.isDirectory) { - // If it's a directory, recursively call this function - scanDirectoryForDatabases( - directory = file, - depth = depth + 1, - foundDatabases = foundDatabases, - ) - } else { - // If it's a file, check if it's a database file - if (file.isFile && - !file.name.endsWith("-wal") && // Write-Ahead Log - !file.name.endsWith("-shm") && // Shared-Memory - !file.name.endsWith("-journal") // Older journaling mode - ) { - foundDatabases.add( - DeviceDataBaseDataModel( - id = file.absolutePath, // Use absolute path for unique ID - name = file.name, - ) - ) - } - } - } - } - - companion object { - private const val MAX_DEPTH = 7 - } -} - - -//private fun executeSelect( -// database: SupportSQLiteDatabase, -// query: String, -//): DatabaseExecuteSqlResponse { -// val cursor: Cursor = database.query(query) -// try { -// val columnNames = cursor.columnNames.toList() -// val rows = cursorToList(cursor) -// return DatabaseExecuteSqlResponse.Select( -// columns = columnNames, -// values = rows, -// ) -// } finally { -// cursor.close() -// } -//} - -//private fun executeUpdateDelete( -// database: SupportSQLiteDatabase, -// query: String, -//): DatabaseExecuteSqlResponse { -// val statement = database.compileStatement(query) -// val count: Int = statement.executeUpdateDelete() -// return DatabaseExecuteSqlResponse.UpdateDelete(count) -//} -// -//private fun executeInsert( -// database: SupportSQLiteDatabase, -// query: String, -//): DatabaseExecuteSqlResponse { -// val statement = database.compileStatement(query) -// val insertedId: Long = statement.executeInsert() -// return DatabaseExecuteSqlResponse.Insert(insertedId) -//} -// -//private fun executeRawQuery( -// database: SupportSQLiteDatabase, -// query: String, -//): DatabaseExecuteSqlResponse { -// database.execSQL(query) -// return DatabaseExecuteSqlResponse.RawSuccess -//} - -private fun getFirstWord(s: String): String { - var s = s - s = s.trim { it <= ' ' } - val firstSpace = s.indexOf(' ') - return if (firstSpace >= 0) s.substring(0, firstSpace) else s -} - -private fun cursorToList(cursor: Cursor): List> { - val rows = mutableListOf>() - val numColumns = cursor.columnCount - while (cursor.moveToNext()) { - val values = mutableListOf() - for (column in 0.. null - Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(column).toString() - Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(column).toString() - Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(column).toString() - Cursor.FIELD_TYPE_STRING -> cursor.getString(column).toString() - else -> cursor.getString(column) - } -} - -// must use the old way to get the version... -private fun getDatabaseVersion( - path: String, -): Int { - return android.database.sqlite.SQLiteDatabase.openDatabase( - path, - null, - android.database.sqlite.SQLiteDatabase.OPEN_READONLY - ).use { db -> - db.rawQuery("PRAGMA user_version", null).use { cursor -> - if (cursor.moveToFirst()) cursor.getInt(0) else 0 - } - } -} \ No newline at end of file diff --git a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.android.kt b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.android.kt new file mode 100644 index 000000000..aae3a3626 --- /dev/null +++ b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.android.kt @@ -0,0 +1,46 @@ +package io.github.openflocon.flocon.database.core.model + +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse + +actual fun openDbAndExecuteQuery( + path: String, + query: String +): DatabaseExecuteSqlResponse { +// var helper: SupportSQLiteOpenHelper? = null +// return try { +// val path = context.getDatabasePath(databaseName) +// val version = getDatabaseVersion(path = path.absolutePath) +// helper = FrameworkSQLiteOpenHelperFactory().create( +// SupportSQLiteOpenHelper.Configuration.builder(context) +// .name(path.absolutePath) +// .callback(object : SupportSQLiteOpenHelper.Callback(version) { +// override fun onCreate(db: SupportSQLiteDatabase) { +// // no op +// } +// +// override fun onUpgrade( +// db: SupportSQLiteDatabase, +// oldVersion: Int, +// newVersion: Int +// ) { +// // no op +// } +// }) +// .build() +// ) +// val database = helper.writableDatabase +// +// executeSQL( +// database = database, +// query = query, +// ) +// } catch (t: Throwable) { +// DatabaseExecuteSqlResponse.Error( +// message = t.message ?: "error on executeSQL", +// originalSql = query, +// ) +// } finally { +// helper?.close() +// } + TODO() +} \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt index 219abe66d..eddb854db 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.database.core import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconEncoding import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol @@ -14,7 +15,8 @@ object FloconDatabase : FloconPluginFactory = mutableListOf() diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.kt index dc07b079b..aff63fe0a 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePluginImpl.kt @@ -1,47 +1,51 @@ package io.github.openflocon.flocon.database.core -import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseDataSource import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseProvider import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DatabaseQueryLogModel +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel import io.github.openflocon.flocon.database.core.model.fromdevice.sql.QueryResultDataModel import io.github.openflocon.flocon.database.core.model.fromdevice.sql.listDeviceDataBaseDataModelToJson import io.github.openflocon.flocon.database.core.model.todevice.DatabaseQueryMessage import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.utils.currentTimeMillis +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update - -internal expect fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString internal class FloconDatabasePluginImpl( private var sender: FloconMessageSender, - private val context: FloconContext, + private val scope: CoroutineScope, @FloconMarker override val providers: List ) : FloconPlugin, FloconDatabasePlugin { - override val key: String = "DATABASE" + override val key: String = Protocol.FromDevice.Database.Plugin private val registeredDatabases = MutableStateFlow>(emptyList()) - private val dataSource = buildFloconDatabaseDataSource(context) - override suspend fun onMessageReceived( method: String, body: String ) { + println("Database: $method & $body") when (method) { Protocol.ToDevice.Database.Method.GetDatabases -> sendAllDatabases(sender) Protocol.ToDevice.Database.Method.Query -> { val queryMessage = DatabaseQueryMessage.fromJson(message = body) ?: return val databaseModel = registeredDatabases.value - .find { it.displayName == queryMessage.database } + .find { it.id == queryMessage.database } + val result = databaseModel?.executeQuery(query = queryMessage.query) ?: DatabaseExecuteSqlResponse.Error( message = "Database not found", @@ -54,7 +58,7 @@ internal class FloconDatabasePluginImpl( method = Protocol.FromDevice.Database.Method.Query, body = QueryResultDataModel( requestId = queryMessage.requestId, - result = result + result = FloconEncoder.json.encodeToString(result) ) .toJson() ) @@ -69,15 +73,17 @@ internal class FloconDatabasePluginImpl( sendAllDatabases(sender) } + @OptIn(FloconMarker::class) private suspend fun sendAllDatabases(sender: FloconMessageSender) { - val databases = dataSource.getAllDataBases( - registeredDatabases = registeredDatabases.value, - ) + val databases = providers.flatMap { it.getAllDataBases(emptyList()) } + val all = registeredDatabases.updateAndGet { it + databases } + .map { DeviceDataBaseDataModel(id = it.id, name = it.displayName) } + try { sender.send( plugin = Protocol.FromDevice.Database.Plugin, method = Protocol.FromDevice.Database.Method.GetDatabases, - body = listDeviceDataBaseDataModelToJson(databases), + body = listDeviceDataBaseDataModelToJson(all), ) } catch (t: Throwable) { FloconLogger.logError("Database parsing error", t) @@ -89,19 +95,22 @@ internal class FloconDatabasePluginImpl( } override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List) { - try { -// sender.send( -// plugin = Protocol.FromDevice.Database.Plugin, -// method = Protocol.FromDevice.Database.Method.LogQuery, -// body = DatabaseQueryLogModel( -// dbName = dbName, -// sqlQuery = sqlQuery, -// bindArgs = bindArgs.map { it.toString() }, -// timestamp = currentTimeMillis(), -// ).toJson(), -// ) - } catch (t: Throwable) { - FloconLogger.logError("Database logging error", t) + scope.launch { + try { + sender.send( + plugin = Protocol.FromDevice.Database.Plugin, + method = Protocol.FromDevice.Database.Method.LogQuery, + body = DatabaseQueryLogModel( + dbName = dbName, + sqlQuery = sqlQuery, + bindArgs = bindArgs.map { it.toString() }, + timestamp = currentTimeMillis() + ) + .toJson() + ) + } catch (t: Throwable) { + FloconLogger.logError("Database logging error", t) + } } } diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseProvider.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseProvider.kt index b54fb9d56..047665840 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseProvider.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseProvider.kt @@ -1,7 +1,6 @@ package io.github.openflocon.flocon.database.core.datasource import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel -import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel import io.github.openflocon.flocon.dsl.FloconMarker interface FloconDatabaseProvider { @@ -9,6 +8,6 @@ interface FloconDatabaseProvider { @FloconMarker fun getAllDataBases( registeredDatabases: List - ): List + ): List } \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt index 8b7899c70..c6978e3f8 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.kt @@ -1,8 +1,10 @@ package io.github.openflocon.flocon.database.core.model import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse interface FloconDatabaseModel { + val id: String val displayName: String suspend fun executeQuery(query: String): DatabaseExecuteResponse @@ -10,10 +12,18 @@ interface FloconDatabaseModel { } data class FloconFileDatabaseModel( + override val id: String, override val displayName: String, val absolutePath: String ) : FloconDatabaseModel { + override suspend fun executeQuery(query: String): DatabaseExecuteResponse { - TODO("Not yet implemented") + return openDbAndExecuteQuery(absolutePath, query) } + } + +expect fun openDbAndExecuteQuery( + path: String, + query: String +): DatabaseExecuteSqlResponse \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt index 337bb00f4..182dcc0e2 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.encodeToString @Serializable data class DeviceDataBaseDataModel( val id: String, - val name: String, + val name: String ) fun listDeviceDataBaseDataModelToJson(items: List) : String { diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt index 1d1da7d6c..a05cfdae1 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.encodeToString @Serializable internal data class QueryResultDataModel( val requestId: String, - val result: DatabaseExecuteResponse, + val result: String ) { fun toJson(): String { return FloconEncoder.json.encodeToString(this) diff --git a/FloconAndroid/database/room/build.gradle.kts b/FloconAndroid/database/room/build.gradle.kts index c828ca467..4b8d7eb40 100644 --- a/FloconAndroid/database/room/build.gradle.kts +++ b/FloconAndroid/database/room/build.gradle.kts @@ -25,7 +25,8 @@ kotlin { implementation(project(":flocon")) api(project(":database:core")) implementation(libs.androidx.room.runtime) - implementation(libs.androidx.sqlite) + implementation(libs.androidx.room.sqlite.wrapper) + implementation(libs.androidx.sqlite.bundled) } } diff --git a/FloconAndroid/database/room/src/androidMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.android.kt b/FloconAndroid/database/room/src/androidMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.android.kt new file mode 100644 index 000000000..68eeae3b9 --- /dev/null +++ b/FloconAndroid/database/room/src/androidMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.android.kt @@ -0,0 +1,75 @@ +package io.github.openflocon.flocon.database.room + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel +import io.github.openflocon.flocon.dsl.FloconMarker +import java.io.File + +@OptIn(markerClass = [FloconMarker::class]) +internal actual class FloconRoomDatabaseProviderImpl actual constructor( + private val context: FloconContext, + paths: List +) : FloconRoomDatabaseProvider { + + actual override fun register() { + TODO("Not yet implemented") + } + + @FloconMarker + actual override fun getAllDataBases(registeredDatabases: List): List { + val databasesDir = context.context.getDatabasePath("dummy_db") + .parentFile + ?: return emptyList() + + val foundDatabases = mutableListOf() + // Start the recursive search from the base databases directory + scanDirectoryForDatabases( + directory = databasesDir, + depth = 0, + foundDatabases = foundDatabases + ) + + return foundDatabases + } + + private fun scanDirectoryForDatabases( + directory: File, + depth: Int, + foundDatabases: MutableList + ) { + if (depth >= MAX_DEPTH) { + return + } + directory.listFiles()?.forEach { file -> + if (file.isDirectory) { + // If it's a directory, recursively call this function + scanDirectoryForDatabases( + directory = file, + depth = depth + 1, + foundDatabases = foundDatabases, + ) + } else { + // If it's a file, check if it's a database file + if (file.isFile && + !file.name.endsWith("-wal") && // Write-Ahead Log + !file.name.endsWith("-shm") && // Shared-Memory + !file.name.endsWith("-journal") // Older journaling mode + ) { + foundDatabases.add( + FloconFileDatabaseModel( + id = file.absolutePath, // Use absolute path for unique ID + displayName = file.name, + absolutePath = file.absolutePath + ) + ) + } + } + } + } + + companion object { + private const val MAX_DEPTH = 7 + } +} \ No newline at end of file diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseConfig.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseConfig.kt index 40d7007f1..f34d1d51d 100644 --- a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseConfig.kt +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseConfig.kt @@ -18,6 +18,7 @@ fun FloconDatabaseConfig.room(block: FloconRoomDatabaseConfig.() -> Unit = {}) { providers.add( FloconRoomDatabaseProviderImpl( + context = context, paths = config.paths ) ) diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt index c97d82cd8..632ce7a9e 100644 --- a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt @@ -10,7 +10,26 @@ import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse -data class FloconRoomDatabaseModel( +fun floconRegisterDatabase(displayName: String, database: RoomDatabase) { + Flocon.databasePlugin.register( + FloconRoomDatabaseModel( + id = displayName, + displayName = displayName, + database = database + ) + ) +} + +fun floconLogDatabaseQuery(databaseName: String, sqlQuery: String, bindArgs: List) { + Flocon.databasePlugin.logQuery( + dbName = databaseName, + sqlQuery = sqlQuery, + bindArgs = bindArgs, + ) +} + +internal data class FloconRoomDatabaseModel( + override val id: String, override val displayName: String, val database: RoomDatabase ) : FloconDatabaseModel { @@ -114,13 +133,4 @@ private fun getFirstWord(s: String): String { val trimmed = s.trim() val firstSpace = trimmed.indexOf(' ') return if (firstSpace >= 0) trimmed.substring(0, firstSpace) else trimmed -} - -fun floconRegisterDatabase(displayName: String, database: RoomDatabase) { - Flocon.databasePlugin.register( - FloconRoomDatabaseModel( - displayName = displayName, - database = database, - ) - ) -} +} \ No newline at end of file diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt index db6fc35b3..7982aeeac 100644 --- a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt @@ -1,10 +1,10 @@ package io.github.openflocon.flocon.database.room import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.database.core.databasePlugin import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseProvider import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel -import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel import io.github.openflocon.flocon.dsl.FloconMarker interface FloconRoomDatabaseProvider : FloconDatabaseProvider { @@ -15,20 +15,12 @@ interface FloconRoomDatabaseProvider : FloconDatabaseProvider { } @OptIn(FloconMarker::class) -internal class FloconRoomDatabaseProviderImpl( +internal expect class FloconRoomDatabaseProviderImpl( + context: FloconContext, paths: List ) : FloconRoomDatabaseProvider { - - override fun getAllDataBases( - registeredDatabases: List - ): List { - TODO("Not yet implemented") - } - - override fun register() { - TODO("Not yet implemented") - } - + override fun register() + override fun getAllDataBases(registeredDatabases: List): List } @OptIn(FloconMarker::class) diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.android.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.android.kt deleted file mode 100644 index 8f40c0b14..000000000 --- a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.android.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.openflocon.flocon.database.room - -import android.database.sqlite.SQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper - -//data class FloconRoomDatabaseModel( -// override val displayName: String, -// override val database: SQLiteDatabase -//) : FloconAndroidSqlDatabaseModel - -fun floconRegisterDatabase(displayName: String, database: SQLiteDatabase) { -// Flocon.databasePlugin.register( -// FloconRoomDatabaseModel( -// displayName = displayName, -// database = database -// ) -// ) -} - -fun floconRegisterDatabase(displayName: String, openHelper: SupportSQLiteOpenHelper) { -// floconRegisterDatabase( -// displayName = displayName, -// database = openHelper.writableDatabase., -// ) -} diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt deleted file mode 100644 index 045b20f29..000000000 --- a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconSqliteDatabaseModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.database.room - -import android.database.sqlite.SQLiteDatabase -import androidx.sqlite.db.SupportSQLiteDatabase - -internal class FloconSqliteDatabaseModel( - override val displayName: String, - override val database: SQLiteDatabase//SupportSQLiteDatabase -) : FloconAndroidSqlDatabaseModel diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/extensions/RoomBuilderExt.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/extensions/RoomBuilderExt.kt new file mode 100644 index 000000000..5c12df0bf --- /dev/null +++ b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/extensions/RoomBuilderExt.kt @@ -0,0 +1,25 @@ +package io.github.openflocon.flocon.database.room.extensions + +import androidx.room.RoomDatabase +import androidx.room.RoomDatabase.QueryCallback +import io.github.openflocon.flocon.database.room.floconLogDatabaseQuery +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +inline fun RoomDatabase.Builder.floconLogs( + name: String? = T::class.simpleName, + executor: Executor = Executors.newSingleThreadExecutor(), + queryCallback: QueryCallback? = null +): RoomDatabase.Builder { + return setQueryCallback( + queryCallback = { sqlQuery, bindArgs -> + floconLogDatabaseQuery( + databaseName = name ?: "database", + sqlQuery = sqlQuery, + bindArgs = bindArgs + ) + queryCallback?.onQuery(sqlQuery, bindArgs) + }, + executor = executor + ) +} \ No newline at end of file diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index 3bae66feb..6caacd52c 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.plugins.deeplinks import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginFactory @@ -12,7 +13,7 @@ import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel object FloconDeeplinks : FloconPluginFactory { override val name: String = "Deeplinks" override val pluginId: String = FloconDeeplinks::class.simpleName!! - override fun createConfig() = FloconDeeplinksConfig() + override fun createConfig(context: FloconContext) = FloconDeeplinksConfig() @OptIn(FloconMarker::class) override fun install( diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt index 075727480..6386e43ef 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -67,6 +67,7 @@ class Flocon internal constructor( } private fun onMessageReceived(message: String) { + println("Message received : $message") config.scope.launch(Dispatchers.IO) { floconMessageFromServerFromJson(message)?.let { messageFromServer -> plugins.find { it.key == messageFromServer.plugin } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index 5d5d0fda1..0d74f842c 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -5,9 +5,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow class FloconConfiguration internal constructor( private val config: FloconConfig @@ -23,7 +20,7 @@ class FloconConfiguration internal constructor( configure: Config.() -> Unit = {} ) { plugins[factory.pluginId] = { scope -> - val config = factory.createConfig() + val config = factory.createConfig(config.context) .apply { configure() } factory.install( diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt index 377e3f4d9..948f13d2b 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -40,7 +40,7 @@ interface FloconPluginFactory { override val name: String = "Analytics" override val pluginId: String = Protocol.ToDevice.Analytics.Plugin - override fun createConfig() = FloconAnalyticsConfig() + override fun createConfig(context: FloconContext) = FloconAnalyticsConfig() override fun install( pluginConfig: FloconAnalyticsConfig, floconConfig: FloconConfig @@ -33,7 +34,7 @@ internal class FloconAnalyticsPluginImpl( private val sender: FloconMessageSender, ) : FloconPlugin, FloconAnalyticsPlugin { override val key: String - get() = "ANALYTICS" + get() = Protocol.ToDevice.Analytics.Plugin override suspend fun onMessageReceived( method: String, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt index 4c31686df..8a61800e1 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt @@ -32,7 +32,8 @@ object FloconCrashReporter : override val pluginId: String = Protocol.ToDevice.Analytics.Plugin // Crash reporter is usually write-only but we can set an ID - override fun createConfig() = FloconCrashReporterConfig() + override fun createConfig(context: FloconContext) = FloconCrashReporterConfig() + override fun install( pluginConfig: FloconCrashReporterConfig, floconConfig: FloconConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt index a556a3880..ee9d2ff6f 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.plugins.dashboard import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig @@ -27,7 +28,7 @@ interface FloconDashboardPlugin : FloconPlugin { object FloconDashboard : FloconPluginFactory { override val name: String = "Dashboard" override val pluginId: String = Protocol.ToDevice.Dashboard.Plugin - override fun createConfig() = FloconDashboardConfig() + override fun createConfig(context: FloconContext) = FloconDashboardConfig() override fun install( pluginConfig: FloconDashboardConfig, floconConfig: FloconConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt index f1a7f9710..8bea31768 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt @@ -18,7 +18,7 @@ interface FloconDevicePlugin : FloconPlugin { object FloconDevice : FloconPluginFactory { override val name: String = "Device" override val pluginId: String = Protocol.ToDevice.Device.Plugin - override fun createConfig() = FloconDeviceConfig() + override fun createConfig(context: FloconContext) = FloconDeviceConfig() override fun install( pluginConfig: FloconDeviceConfig, floconConfig: FloconConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt index a3c708973..3dd6df539 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt @@ -29,7 +29,7 @@ interface FloconFilesPlugin : FloconPlugin object FloconFiles : FloconPluginFactory { override val name: String = "Files" override val pluginId: String = Protocol.ToDevice.Files.Plugin - override fun createConfig() = FloconFilesConfig() + override fun createConfig(context: FloconContext) = FloconFilesConfig() override fun install( pluginConfig: FloconFilesConfig, floconConfig: FloconConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt index f2d162865..79ba0dd4d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -19,7 +19,7 @@ interface FloconPreferencesPlugin : FloconPlugin { object FloconPreferences : FloconPluginFactory { override val name: String = "Preferences" override val pluginId: String = Protocol.ToDevice.SharedPreferences.Plugin - override fun createConfig() = FloconPreferencesConfig() + override fun createConfig(context: FloconContext) = FloconPreferencesConfig() override fun install( pluginConfig: FloconPreferencesConfig, floconConfig: FloconConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt index e22068da7..8c63f218d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocon.plugins.tables import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig @@ -19,7 +20,7 @@ interface FloconTablePlugin : FloconPlugin { object FloconTable : FloconPluginFactory { override val name: String = "Table" override val pluginId: String = Protocol.ToDevice.Table.Plugin - override fun createConfig() = FloconTableConfig() + override fun createConfig(context: FloconContext) = FloconTableConfig() override fun install( pluginConfig: FloconTableConfig, floconConfig: FloconConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt index e48831806..63f2a342e 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.pluginsold.analytics import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory @@ -13,7 +14,7 @@ class FloconAnalyticsConfig : FloconPluginConfig * Flocon Analytics Plugin. */ object FloconAnalytics : FloconPluginFactory { - override fun createConfig(): FloconAnalyticsConfig = TODO() + override fun createConfig(context: FloconContext) = TODO() override fun install( pluginConfig: FloconAnalyticsConfig, floconConfig: FloconConfig diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt index 71f0e8b94..c0b9cfa2b 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt @@ -8,7 +8,7 @@ class FloconDeviceConfig : FloconPluginConfig * Flocon Device Plugin. */ object FloconDevice : FloconPluginFactory { - override fun createConfig(): FloconDeviceConfig { + override fun createConfig(context: FloconContext): FloconDeviceConfig { TODO("Not yet implemented") } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt index 9472865f7..42eebfe1f 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.pluginsold.files import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory @@ -15,7 +16,7 @@ class FloconFilesConfig : FloconPluginConfig { * Used to inspect and download files from the device. */ object FloconFiles : FloconPluginFactory { - override fun createConfig(): FloconFilesConfig { + override fun createConfig(context: FloconContext): FloconFilesConfig { TODO("Not yet implemented") } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt index 3ec1427d2..dec9be9dc 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.pluginsold.sharedprefs import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory @@ -14,7 +15,7 @@ class FloconPreferencesConfig : FloconPluginConfig * Used to inspect SharedPreferences or other key-value stores. */ object FloconPreferences : FloconPluginFactory { - override fun createConfig(): FloconPreferencesConfig { + override fun createConfig(context: FloconContext): FloconPreferencesConfig { TODO("Not yet implemented") } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt index d7b2aec3a..63ebbc40b 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.pluginsold.tables import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory @@ -14,7 +15,7 @@ class FloconTableConfig : FloconPluginConfig * Used to display custom data tables. */ object FloconTable : FloconPluginFactory { - override fun createConfig(): FloconTableConfig { + override fun createConfig(context: FloconContext): FloconTableConfig { TODO("Not yet implemented") } diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index 32ea39e80..f65bafb13 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -19,7 +19,7 @@ composeBom = "2025.06.01" appcompat = "1.7.1" material = "1.12.0" okhttpBom = "4.12.0" -room = "2.7.2" +room = "2.8.4" # for grpc gson = "2.11.0" grpc = "1.70.0" @@ -28,7 +28,7 @@ grpcKotlin = "1.4.3" protobuf = "4.26.1" ksp = "2.1.0-1.0.29" processPhoenix = "3.0.0" -sqlite = "2.5.2" +sqlite = "2.6.2" sqliteJdbc = "3.50.3.0" buildconfig = "5.6.8" brotli = "0.1.2" @@ -36,9 +36,6 @@ brotli = "0.1.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } -androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } -androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } -androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } brotli-dec = { module = "org.brotli:dec", version.ref = "brotli" } apollo-http-okhttprealization = { module = "com.apollographql.apollo:apollo-http-okhttprealization", version.ref = "apollo" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" } @@ -90,9 +87,11 @@ protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin- protobuf-util = { group = "com.google.protobuf", name = "protobuf-java-util", version.ref = "protobuf" } sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" } squareup-okhttp = { module = "com.squareup.okhttp3:okhttp" } -sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } -androidx-sqlite = { group = "androidx.sqlite", name = "sqlite", version.ref = "sqlite" } -androidx-sqlite-framework = { group = "androidx.sqlite", name = "sqlite-framework", version.ref = "sqlite" } + +androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt index 636aada5e..a9eac6957 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.network.core import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory @@ -44,7 +45,7 @@ object FloconNetwork : FloconPluginFactory @@ -243,11 +243,4 @@ class MainActivity : ComponentActivity() { } } - private fun toMigrate() { -// floconRegisterDatabase( -// displayName = "inmemory_dogs", -// openHelper = it.openHelper -// ) - } - } \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt index c499b51df..e3c13da9c 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt @@ -4,11 +4,11 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import io.github.openflocon.flocon.database.room.extensions.floconLogs import io.github.openflocon.flocon.myapplication.database.dao.DogDao import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.database.model.HumanEntity import io.github.openflocon.flocon.myapplication.database.model.HumanWithDogEntity -import java.util.concurrent.Executors @Database( entities = [ @@ -29,18 +29,16 @@ abstract class DogDatabase : RoomDatabase() { fun getDatabase(context: Context): DogDatabase { val dbName = "dogs_database" return INSTANCE ?: synchronized(this) { - TODO() -// val instance = Room.databaseBuilder( -// context.applicationContext, -// DogDatabase::class.java, -// dbName -// ).setQueryCallback({ sqlQuery, bindArgs -> floconLogDatabaseQuery( -// dbName = dbName, sqlQuery = sqlQuery, bindArgs = bindArgs -// ) }, Executors.newSingleThreadExecutor()) -// .fallbackToDestructiveMigration() -// .build() -// INSTANCE = instance -// instance + val instance = Room.databaseBuilder( + context.applicationContext, + DogDatabase::class.java, + dbName + ) + .floconLogs() + .fallbackToDestructiveMigration(dropAllTables = true) + .build() + INSTANCE = instance + instance } } } diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt index c0da1d077..d380b0602 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.myapplication.database import android.content.Context import androidx.room.Room +import io.github.openflocon.flocon.database.room.floconRegisterDatabase import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.database.model.FoodEntity import io.github.openflocon.flocon.myapplication.database.model.HumanEntity @@ -14,10 +15,10 @@ fun initializeInMemoryDatabases(context: Context): DogDatabase { context, DogDatabase::class.java, ).build().also { -// floconRegisterDatabase( -// displayName = "inmemory_dogs", -// openHelper = it.openHelper -// ) + floconRegisterDatabase( + displayName = "inmemory_dogs", + database = it + ) } } @@ -25,6 +26,11 @@ fun initializeDatabases(context: Context) { val dogDatabase = DogDatabase.getDatabase(context) val foodDatabase = FoodDatabase.getDatabase(context) + floconRegisterDatabase( + displayName = "dogs", + database = dogDatabase + ) + GlobalScope.launch { dogDatabase.dogDao().insertDog( DogEntity( @@ -125,7 +131,7 @@ fun initializeDatabases(context: Context) { id = 10L + i, name = "dog$i", breed = randomBreed, - pictureUrl = "https://picsum.photos/500/50${i%10}.jpg", + pictureUrl = "https://picsum.photos/500/50${i % 10}.jpg", age = (1..15).random() ) ) diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index 51ba4847a..64301aaa6 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -75,7 +75,7 @@ kotlin { implementation(libs.ktor.client.cio) implementation(libs.sqlite.jdbc) - implementation(libs.sqlite.bundled) + implementation(libs.androidx.sqlite.bundled) // Compose Desktop implementation(compose.desktop.currentOs) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/DatabaseTabViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/DatabaseTabViewModel.kt index b28f5c9f0..3c63480f9 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/DatabaseTabViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/DatabaseTabViewModel.kt @@ -59,7 +59,7 @@ class DatabaseTabViewModel( var query = mutableStateOf("") val lastQueries = observeLastSuccessQueriesUseCase(params.databaseId) - .map { it.filterNot { it.isBlank() } } + .map { it.filterNot(String::isBlank) } .flowOn(dispatcherProvider.data) .stateIn( viewModelScope, From 0e8949654aba388c778668c5f9aef3e11e4c563c Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 25 Mar 2026 13:26:31 +0100 Subject: [PATCH 17/38] fix: Database --- FloconAndroid/.vscode/settings.json | 2 +- FloconAndroid/database/core/build.gradle.kts | 2 + .../core/model/FloconDatabaseModel.android.kt | 128 +++++++--- .../datasource/FloconDatabaseDataSource.kt | 1 - .../database/core/FloconDatabasePlugin.ios.kt | 126 +--------- .../core/model/FloconDatabaseModel.ios.kt | 75 ++++++ .../database/core/FloconDatabasePlugin.jvm.kt | 90 ++++++- .../FloconRoomDatabaseProviderImpl.android.kt | 6 +- .../database/room/FloconRoomDatabaseModel.kt | 3 - .../room/FloconRoomDatabaseProviderImpl.kt | 1 - .../room/FloconDatabasePlugin.android.kt | 231 ------------------ 11 files changed, 260 insertions(+), 405 deletions(-) create mode 100644 FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.ios.kt delete mode 100644 FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt diff --git a/FloconAndroid/.vscode/settings.json b/FloconAndroid/.vscode/settings.json index c5f3f6b9c..e0f15db2e 100644 --- a/FloconAndroid/.vscode/settings.json +++ b/FloconAndroid/.vscode/settings.json @@ -1,3 +1,3 @@ { - "java.configuration.updateBuildConfiguration": "interactive" + "java.configuration.updateBuildConfiguration": "automatic" } \ No newline at end of file diff --git a/FloconAndroid/database/core/build.gradle.kts b/FloconAndroid/database/core/build.gradle.kts index 1d07bb50d..f66e064ad 100644 --- a/FloconAndroid/database/core/build.gradle.kts +++ b/FloconAndroid/database/core/build.gradle.kts @@ -37,6 +37,7 @@ kotlin { val jvmMain by getting { dependencies { + implementation(libs.sqlite.jdbc) } } @@ -49,6 +50,7 @@ kotlin { iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) dependencies { + implementation(libs.androidx.sqlite.bundled) } } } diff --git a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.android.kt b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.android.kt index aae3a3626..a0157ddcc 100644 --- a/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.android.kt +++ b/FloconAndroid/database/core/src/androidMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.android.kt @@ -1,46 +1,100 @@ package io.github.openflocon.flocon.database.core.model +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import java.util.Locale actual fun openDbAndExecuteQuery( path: String, query: String ): DatabaseExecuteSqlResponse { -// var helper: SupportSQLiteOpenHelper? = null -// return try { -// val path = context.getDatabasePath(databaseName) -// val version = getDatabaseVersion(path = path.absolutePath) -// helper = FrameworkSQLiteOpenHelperFactory().create( -// SupportSQLiteOpenHelper.Configuration.builder(context) -// .name(path.absolutePath) -// .callback(object : SupportSQLiteOpenHelper.Callback(version) { -// override fun onCreate(db: SupportSQLiteDatabase) { -// // no op -// } -// -// override fun onUpgrade( -// db: SupportSQLiteDatabase, -// oldVersion: Int, -// newVersion: Int -// ) { -// // no op -// } -// }) -// .build() -// ) -// val database = helper.writableDatabase -// -// executeSQL( -// database = database, -// query = query, -// ) -// } catch (t: Throwable) { -// DatabaseExecuteSqlResponse.Error( -// message = t.message ?: "error on executeSQL", -// originalSql = query, -// ) -// } finally { -// helper?.close() -// } - TODO() + var database: SQLiteDatabase? = null + return try { + database = SQLiteDatabase.openDatabase( + path, + null, + SQLiteDatabase.OPEN_READWRITE + ) + executeSQLInternal(database, query) + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "error on executeSQL", + originalSql = query, + ) + } finally { + database?.close() + } +} + +private fun executeSQLInternal( + database: SQLiteDatabase, + query: String +): DatabaseExecuteSqlResponse { + val firstWord = query.trim().let { + val idx = it.indexOf(' ') + if (idx >= 0) it.substring(0, idx) else it + }.uppercase(Locale.getDefault()) + + return when (firstWord) { + "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(database, query) + "INSERT" -> executeInsert(database, query) + "UPDATE", "DELETE" -> executeUpdateDelete(database, query) + else -> executeRawQuery(database, query) + } +} + +private fun executeSelect( + database: SQLiteDatabase, + query: String, +): DatabaseExecuteSqlResponse { + val cursor: Cursor = database.rawQuery(query, null) + return try { + val columnNames = cursor.columnNames.toList() + val rows = mutableListOf>() + while (cursor.moveToNext()) { + val values = mutableListOf() + for (i in 0 until cursor.columnCount) { + values.add( + when (cursor.getType(i)) { + Cursor.FIELD_TYPE_NULL -> null + Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(i).toString() + Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(i).toString() + Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(i).toString() + else -> cursor.getString(i) + } + ) + } + rows.add(values) + } + DatabaseExecuteSqlResponse.Select(columns = columnNames, values = rows) + } finally { + cursor.close() + } +} + +private fun executeInsert( + database: SQLiteDatabase, + query: String, +): DatabaseExecuteSqlResponse { + val statement = database.compileStatement(query) + val insertedId: Long = statement.executeInsert() + return DatabaseExecuteSqlResponse.Insert(insertedId) +} + +private fun executeUpdateDelete( + database: SQLiteDatabase, + query: String, +): DatabaseExecuteSqlResponse { + val statement = database.compileStatement(query) + val count: Int = statement.executeUpdateDelete() + return DatabaseExecuteSqlResponse.UpdateDelete(count) +} + +private fun executeRawQuery( + database: SQLiteDatabase, + query: String, +): DatabaseExecuteSqlResponse { + database.execSQL(query) + return DatabaseExecuteSqlResponse.RawSuccess } \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt index 8415d9e79..53e00734b 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt @@ -2,7 +2,6 @@ package io.github.openflocon.flocon.database.core.datasource import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel interface FloconDatabaseDataSource { diff --git a/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt b/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt index b6b4286b7..3ec4ab462 100644 --- a/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt +++ b/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.ios.kt @@ -1,127 +1,3 @@ package io.github.openflocon.flocon.database.core -import androidx.sqlite.SQLiteConnection -import androidx.sqlite.driver.NativeSQLiteDriver -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel -import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel -import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel -import platform.Foundation.NSFileManager - -internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { - return FloconDatabaseDataSourceIos(context) -} - -internal class FloconDatabaseDataSourceIos( - private val context: FloconContext -) : FloconDatabaseDataSource { - - override fun executeSQL( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteSqlResponse { - val fileManager = NSFileManager.defaultManager - if (!fileManager.fileExistsAtPath(databaseName)) { - return DatabaseExecuteSqlResponse.Error( - message = "Database file not found: $databaseName", - originalSql = query - ) - } - - val driver = NativeSQLiteDriver() - val connection = driver.open(fileName = databaseName) - - return try { - val firstWord = getFirstWord(query).uppercase() - when (firstWord) { - "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) - "INSERT" -> executeInsert(connection, query) - "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) - else -> executeRawQuery(connection, query) - } - } catch (t: Throwable) { - DatabaseExecuteSqlResponse.Error( - message = t.message ?: "Error executing SQL", - originalSql = query - ) - } finally { - connection.close() - } - } - - override fun getAllDataBases( - registeredDatabases: List - ): List { - val fileManager = NSFileManager.defaultManager - return registeredDatabases.mapNotNull { - if(it is FloconFileDatabaseModel) { - if (fileManager.fileExistsAtPath(it.absolutePath)) { - DeviceDataBaseDataModel( - id = it.absolutePath, - name = it.displayName - ) - } else null - } else null - } - } -} - -// --- SQL execution helpers --- - -private fun executeSelect(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { - val cursor = connection.prepare(query).use { statement -> - val columnCount = statement.getColumnCount() - val columns = (0 until columnCount).map { statement.getColumnName(it) } - val rows = mutableListOf>() - - while (statement.step()) { - val row = (0 until columnCount).map { idx -> - statement.getText(idx) - } - rows.add(row) - } - - statement.close() // maybe remove - DatabaseExecuteSqlResponse.Select(columns, rows) - } - return cursor -} - -private fun executeUpdateDelete(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { - connection.prepare(query).use { statement -> - statement.close() - } - // sqlite-kt n'expose pas encore `changes()`, on renvoie 0 - return DatabaseExecuteSqlResponse.UpdateDelete(affectedCount = 0) -} - -private fun executeInsert(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { - connection.prepare(query).use { statement -> - statement.close() - } - - // Récupération du dernier ID inséré - var id = -1L - connection.prepare("SELECT last_insert_rowid()").use { -// id = if (it.step()) it.getLong(0) else -1L -// it.close() // maybe remove - } - - return DatabaseExecuteSqlResponse.Insert(id) -} - -private fun executeRawQuery(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { - connection.prepare(query).use { statement -> - statement.close() // maybe remove - } - return DatabaseExecuteSqlResponse.RawSuccess -} - -// --- Utilities --- -private fun getFirstWord(s: String): String { - val trimmed = s.trim() - val firstSpace = trimmed.indexOf(' ') - return if (firstSpace >= 0) trimmed.substring(0, firstSpace) else trimmed -} +// iOS implementation is in model/FloconDatabaseModel.ios.kt diff --git a/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.ios.kt b/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.ios.kt new file mode 100644 index 000000000..7b8ddc3d6 --- /dev/null +++ b/FloconAndroid/database/core/src/iosMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.ios.kt @@ -0,0 +1,75 @@ +package io.github.openflocon.flocon.database.core.model + +import androidx.sqlite.SQLiteConnection +import androidx.sqlite.driver.NativeSQLiteDriver +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse + +actual fun openDbAndExecuteQuery( + path: String, + query: String +): DatabaseExecuteSqlResponse { + val driver = NativeSQLiteDriver() + val connection = driver.open(fileName = path) + return try { + val firstWord = query.trim().let { + val idx = it.indexOf(' ') + if (idx >= 0) it.substring(0, idx) else it + }.uppercase() + + when (firstWord) { + "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) + "INSERT" -> executeInsert(connection, query) + "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) + else -> executeRawQuery(connection, query) + } + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "Error executing SQL", + originalSql = query + ) + } finally { + connection.close() + } +} + +// --- SQL execution helpers --- + +private fun executeSelect(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + return connection.prepare(query).use { statement -> + val columnCount = statement.getColumnCount() + val columns = (0 until columnCount).map { statement.getColumnName(it) } + val rows = mutableListOf>() + + while (statement.step()) { + val row = (0 until columnCount).map { idx -> + if (statement.isNull(idx)) null else statement.getText(idx) + } + rows.add(row) + } + + DatabaseExecuteSqlResponse.Select(columns, rows) + } +} + +private fun executeInsert(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { it.step() } + + val id = connection.prepare("SELECT last_insert_rowid()").use { stmt -> + if (stmt.step()) stmt.getLong(0) else -1L + } + return DatabaseExecuteSqlResponse.Insert(id) +} + +private fun executeUpdateDelete(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { it.step() } + + val count = connection.prepare("SELECT changes()").use { stmt -> + if (stmt.step()) stmt.getLong(0).toInt() else 0 + } + return DatabaseExecuteSqlResponse.UpdateDelete(affectedCount = count) +} + +private fun executeRawQuery(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { it.step() } + return DatabaseExecuteSqlResponse.RawSuccess +} \ No newline at end of file diff --git a/FloconAndroid/database/core/src/jvmMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.jvm.kt b/FloconAndroid/database/core/src/jvmMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.jvm.kt index e0c25951c..fde7260cf 100644 --- a/FloconAndroid/database/core/src/jvmMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.jvm.kt +++ b/FloconAndroid/database/core/src/jvmMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabasePlugin.jvm.kt @@ -1,7 +1,89 @@ -package io.github.openflocon.flocon.database.core +package io.github.openflocon.flocon.database.core.model -import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse +import java.sql.Connection +import java.sql.DriverManager +import java.sql.ResultSet -internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { - TODO("Not yet implemented") +actual fun openDbAndExecuteQuery( + path: String, + query: String +): DatabaseExecuteSqlResponse { + var connection: Connection? = null + return try { + Class.forName("org.sqlite.JDBC") + connection = DriverManager.getConnection("jdbc:sqlite:$path") + executeSQLInternal(connection, query) + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "error on executeSQL", + originalSql = query, + ) + } finally { + connection?.close() + } +} + +private fun executeSQLInternal( + connection: Connection, + query: String +): DatabaseExecuteSqlResponse { + val firstWord = query.trim().let { + val idx = it.indexOf(' ') + if (idx >= 0) it.substring(0, idx) else it + }.uppercase() + + return when (firstWord) { + "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) + "INSERT" -> executeInsert(connection, query) + "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) + else -> executeRawQuery(connection, query) + } +} + +private fun executeSelect( + connection: Connection, + query: String, +): DatabaseExecuteSqlResponse { + val statement = connection.createStatement() + val rs: ResultSet = statement.executeQuery(query) + val meta = rs.metaData + val columnCount = meta.columnCount + val columns = (1..columnCount).map { meta.getColumnName(it) } + val rows = mutableListOf>() + while (rs.next()) { + val row = (1..columnCount).map { i -> + rs.getString(i) + } + rows.add(row) + } + return DatabaseExecuteSqlResponse.Select(columns = columns, values = rows) +} + +private fun executeInsert( + connection: Connection, + query: String, +): DatabaseExecuteSqlResponse { + val statement = connection.createStatement() + statement.executeUpdate(query) + val keys: ResultSet = statement.generatedKeys + val insertedId = if (keys.next()) keys.getLong(1) else -1L + return DatabaseExecuteSqlResponse.Insert(insertedId) +} + +private fun executeUpdateDelete( + connection: Connection, + query: String, +): DatabaseExecuteSqlResponse { + val statement = connection.createStatement() + val count = statement.executeUpdate(query) + return DatabaseExecuteSqlResponse.UpdateDelete(count) +} + +private fun executeRawQuery( + connection: Connection, + query: String, +): DatabaseExecuteSqlResponse { + connection.createStatement().execute(query) + return DatabaseExecuteSqlResponse.RawSuccess } \ No newline at end of file diff --git a/FloconAndroid/database/room/src/androidMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.android.kt b/FloconAndroid/database/room/src/androidMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.android.kt index 68eeae3b9..2626606a4 100644 --- a/FloconAndroid/database/room/src/androidMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.android.kt +++ b/FloconAndroid/database/room/src/androidMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.android.kt @@ -1,9 +1,10 @@ package io.github.openflocon.flocon.database.room +import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.databasePlugin import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel -import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel import io.github.openflocon.flocon.dsl.FloconMarker import java.io.File @@ -14,7 +15,8 @@ internal actual class FloconRoomDatabaseProviderImpl actual constructor( ) : FloconRoomDatabaseProvider { actual override fun register() { - TODO("Not yet implemented") + val databases = getAllDataBases(emptyList()) + databases.forEach { Flocon.databasePlugin.register(it) } } @FloconMarker diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt index 632ce7a9e..ce1ad90c5 100644 --- a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseModel.kt @@ -105,9 +105,6 @@ private suspend fun executeInsert( query: String, ): DatabaseExecuteSqlResponse { connection.execSQL(query) - // SQLite doesn't easily return the last inserted ID via the statement itself without extra queries like last_insert_rowid() - // But for inspection purposes, we might just return 0 or query it. - // For now, let's just return a successful RawSuccess or implement last_insert_rowid val id = connection.usePrepared("SELECT last_insert_rowid()") { it.step(); it.getLong(0) } return DatabaseExecuteSqlResponse.Insert(id) } diff --git a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt index 7982aeeac..112a4ee42 100644 --- a/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt +++ b/FloconAndroid/database/room/src/commonMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.kt @@ -9,7 +9,6 @@ import io.github.openflocon.flocon.dsl.FloconMarker interface FloconRoomDatabaseProvider : FloconDatabaseProvider { - // TODO fun register() } diff --git a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt b/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt deleted file mode 100644 index fce42a575..000000000 --- a/FloconAndroid/database/room/src/main/java/io/github/openflocon/flocon/database/room/FloconDatabasePlugin.android.kt +++ /dev/null @@ -1,231 +0,0 @@ -package io.github.openflocon.flocon.database.room - -import android.content.Context -import android.database.Cursor -import android.database.sqlite.SQLiteDatabase -import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseDataSource -import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel -import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel -import java.io.File -import java.util.Locale - -interface FloconAndroidSqlDatabaseModel : FloconDatabaseModel { - val database: SQLiteDatabase - - - override suspend fun executeQuery(query: String): DatabaseExecuteResponse { - return executeSQLInternal(database, query) - } -} - -internal class FloconDatabaseDataSourceAndroid(private val context: Context) : - FloconDatabaseDataSource { - - private val MAX_DEPTH = 7 - - override suspend fun executeQuery( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteResponse? { - val databaseModel = registeredDatabases.find { it.displayName == databaseName } - return when (databaseModel) { - is FloconAndroidSqlDatabaseModel -> { - executeSQLInternal( - database = databaseModel.database, - query = query, - ) - } - - else -> openDbAndExecuteQuery( - databaseName = databaseName, - query = query, - ) - } - } - - private fun openDbAndExecuteQuery( - databaseName: String, - query: String - ): DatabaseExecuteSqlResponse { - var database: SQLiteDatabase? = null - return try { - val path = context.getDatabasePath(databaseName) - database = SQLiteDatabase.openDatabase( - path.absolutePath, - null, - SQLiteDatabase.OPEN_READWRITE - ) - - executeSQLInternal( - database = database, - query = query, - ) - } catch (t: Throwable) { - DatabaseExecuteSqlResponse.Error( - message = t.message ?: "error on executeSQL", - originalSql = query, - ) - } finally { - database?.close() - } - } - - override fun getAllDataBases(registeredDatabases: List): List { - val databasesDir = context.getDatabasePath("dummy_db").parentFile ?: return emptyList() - - val foundDatabases = mutableListOf() - // Start the recursive search from the base databases directory - scanDirectoryForDatabases( - directory = databasesDir, - depth = 0, - foundDatabases = foundDatabases - ) - - return foundDatabases - } - - private fun scanDirectoryForDatabases( - directory: File, - depth: Int, - foundDatabases: MutableList - ) { - if (depth >= MAX_DEPTH) { - return - } - directory.listFiles()?.forEach { file -> - if (file.isDirectory) { - // If it's a directory, recursively call this function - scanDirectoryForDatabases( - directory = file, - depth = depth + 1, - foundDatabases = foundDatabases, - ) - } else { - // If it's a file, check if it's a database file - if (file.isFile && - !file.name.endsWith("-wal") && // Write-Ahead Log - !file.name.endsWith("-shm") && // Shared-Memory - !file.name.endsWith("-journal") // Older journaling mode - ) { - foundDatabases.add( - DeviceDataBaseDataModel( - id = file.absolutePath, // Use absolute path for unique ID - name = file.name, - ) - ) - } - } - } - } -} - -private fun executeSQLInternal( - database: SQLiteDatabase, - query: String -): DatabaseExecuteSqlResponse { - return try { - val firstWordUpperCase = getFirstWord(query).uppercase(Locale.getDefault()) - when (firstWordUpperCase) { - "UPDATE", "DELETE" -> executeUpdateDelete(database, query) - "INSERT" -> executeInsert(database, query) - "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(database, query) - else -> executeRawQuery(database, query) - } - } catch (t: Throwable) { - DatabaseExecuteSqlResponse.Error( - message = t.message ?: "error on executeSQL", - originalSql = query, - ) - } -} - -private fun executeSelect( - database: SQLiteDatabase, - query: String, -): DatabaseExecuteSqlResponse { - val cursor: Cursor = database.rawQuery(query, null) - try { - val columnNames = cursor.columnNames.toList() - val rows = cursorToList(cursor) - return DatabaseExecuteSqlResponse.Select( - columns = columnNames, - values = rows, - ) - } finally { - cursor.close() - } -} - -private fun executeUpdateDelete( - database: SQLiteDatabase, - query: String, -): DatabaseExecuteSqlResponse { - val statement = database.compileStatement(query) - val count: Int = statement.executeUpdateDelete() - return DatabaseExecuteSqlResponse.UpdateDelete(count) -} - -private fun executeInsert( - database: SQLiteDatabase, - query: String, -): DatabaseExecuteSqlResponse { - val statement = database.compileStatement(query) - val insertedId: Long = statement.executeInsert() - return DatabaseExecuteSqlResponse.Insert(insertedId) -} - -private fun executeRawQuery( - database: SQLiteDatabase, - query: String, -): DatabaseExecuteSqlResponse { - database.execSQL(query) - return DatabaseExecuteSqlResponse.RawSuccess -} - -private fun getFirstWord(s: String): String { - var s = s - s = s.trim { it <= ' ' } - val firstSpace = s.indexOf(' ') - return if (firstSpace >= 0) s.substring(0, firstSpace) else s -} - -private fun cursorToList(cursor: Cursor): List> { - val rows = mutableListOf>() - val numColumns = cursor.columnCount - while (cursor.moveToNext()) { - val values = mutableListOf() - for (column in 0.. null - Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(column).toString() - Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(column).toString() - Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(column).toString() - Cursor.FIELD_TYPE_STRING -> cursor.getString(column).toString() - else -> cursor.getString(column) - } -} - -private fun getDatabaseVersion( - path: String, -): Int { - return SQLiteDatabase.openDatabase( - path, - null, - SQLiteDatabase.OPEN_READONLY - ).use { db -> - db.rawQuery("PRAGMA user_version", null).use { cursor -> - if (cursor.moveToFirst()) cursor.getInt(0) else 0 - } - } -} From 62c0bebfe3689d5af7ea195f1d52ec3e184ec9ba Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 25 Mar 2026 13:28:59 +0100 Subject: [PATCH 18/38] fix: Remove useless file --- .../datasource/FloconDatabaseDataSource.kt | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt deleted file mode 100644 index 53e00734b..000000000 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/datasource/FloconDatabaseDataSource.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.openflocon.flocon.database.core.datasource - -import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel -import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse -import io.github.openflocon.flocon.database.core.model.fromdevice.sql.DeviceDataBaseDataModel - -interface FloconDatabaseDataSource { - - suspend fun executeQuery( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteResponse? - - fun getAllDataBases( - registeredDatabases: List - ): List - -} \ No newline at end of file From 2688a2430e3f283533f2bb31bcd430c6c6970c96 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 25 Mar 2026 15:40:44 +0100 Subject: [PATCH 19/38] feat: Add room3 --- .../database/room3-no-op/build.gradle.kts | 106 ++++++++++++++ .../database/room3/floconRegisterDatabase.kt | 9 ++ .../database/room3/floconRegisterDatabase.kt | 9 ++ FloconAndroid/database/room3/build.gradle.kts | 121 ++++++++++++++++ ...FloconRoom3DatabaseProviderImpl.android.kt | 77 ++++++++++ .../room3/FloconRoom3DatabaseConfig.kt | 25 ++++ .../room3/FloconRoom3DatabaseModel.kt | 133 ++++++++++++++++++ .../room3/FloconRoom3DatabaseProviderImpl.kt | 29 ++++ .../FloconRoom3DatabaseProviderImpl.ios.kt | 24 ++++ .../FloconRoom3DatabaseProviderImpl.jvm.kt | 24 ++++ .../room3/extensions/Room3BuilderExt.kt | 15 ++ FloconAndroid/flocon/build.gradle.kts | 1 + .../io/github/openflocon/flocon/FloconCore.kt | 1 - .../plugins/files/FloconFilesPlugin.ios.kt | 2 + .../flocon/websocket/FloconHttpClient.ios.kt | 3 + FloconAndroid/gradle/libs.versions.toml | 3 + .../sample-android-only/build.gradle.kts | 2 + FloconAndroid/settings.gradle.kts | 2 + 18 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 FloconAndroid/database/room3-no-op/build.gradle.kts create mode 100644 FloconAndroid/database/room3-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt create mode 100644 FloconAndroid/database/room3-no-op/src/main/java/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt create mode 100644 FloconAndroid/database/room3/build.gradle.kts create mode 100644 FloconAndroid/database/room3/src/androidMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.android.kt create mode 100644 FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt create mode 100644 FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt create mode 100644 FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.kt create mode 100644 FloconAndroid/database/room3/src/iosMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.ios.kt create mode 100644 FloconAndroid/database/room3/src/jvmMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.jvm.kt create mode 100644 FloconAndroid/database/room3/src/main/java/io/github/openflocon/flocon/database/room3/extensions/Room3BuilderExt.kt diff --git a/FloconAndroid/database/room3-no-op/build.gradle.kts b/FloconAndroid/database/room3-no-op/build.gradle.kts new file mode 100644 index 000000000..5c3db1e82 --- /dev/null +++ b/FloconAndroid/database/room3-no-op/build.gradle.kts @@ -0,0 +1,106 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":database:core-no-op")) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "io.github.openflocon.flocon.database.room3.noop" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-database-room3-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Room 3 Implementation No-Op" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/database/room3-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt b/FloconAndroid/database/room3-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt new file mode 100644 index 000000000..e5f4b0a89 --- /dev/null +++ b/FloconAndroid/database/room3-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.database.room3 + +fun floconRegisterDatabase(displayName: String, database: Any) { + // no op +} + +fun floconRegisterDatabase(displayName: String, openHelper: Any, dummy: Boolean = true) { + // no op +} diff --git a/FloconAndroid/database/room3-no-op/src/main/java/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt b/FloconAndroid/database/room3-no-op/src/main/java/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt new file mode 100644 index 000000000..e5f4b0a89 --- /dev/null +++ b/FloconAndroid/database/room3-no-op/src/main/java/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.database.room3 + +fun floconRegisterDatabase(displayName: String, database: Any) { + // no op +} + +fun floconRegisterDatabase(displayName: String, openHelper: Any, dummy: Boolean = true) { + // no op +} diff --git a/FloconAndroid/database/room3/build.gradle.kts b/FloconAndroid/database/room3/build.gradle.kts new file mode 100644 index 000000000..99e8d69c1 --- /dev/null +++ b/FloconAndroid/database/room3/build.gradle.kts @@ -0,0 +1,121 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.androidx.room) + alias(libs.plugins.ksp) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(project(":database:core")) + implementation(libs.androidx.room3.runtime) + implementation(libs.androidx.sqlite.bundled) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +room { + schemaDirectory("$projectDir/schemas") +} + +dependencies { + // KSP for Room + add("kspCommonMainMetadata", libs.androidx.room3.compiler) + add("kspAndroid", libs.androidx.room3.compiler) + add("kspJvm", libs.androidx.room3.compiler) + add("kspIosArm64", libs.androidx.room3.compiler) + add("kspIosSimulatorArm64", libs.androidx.room3.compiler) +} + +android { + namespace = "io.github.openflocon.flocon.database.room3" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-database-room3", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Room 3 Implementation" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/database/room3/src/androidMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.android.kt b/FloconAndroid/database/room3/src/androidMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.android.kt new file mode 100644 index 000000000..4dcc4d16b --- /dev/null +++ b/FloconAndroid/database/room3/src/androidMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.android.kt @@ -0,0 +1,77 @@ +package io.github.openflocon.flocon.database.room3 + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.databasePlugin +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel +import io.github.openflocon.flocon.dsl.FloconMarker +import java.io.File + +@OptIn(markerClass = [FloconMarker::class]) +internal actual class FloconRoom3DatabaseProviderImpl actual constructor( + private val context: FloconContext, + paths: List +) : FloconRoom3DatabaseProvider { + + actual override fun register() { + val databases = getAllDataBases(emptyList()) + databases.forEach { Flocon.databasePlugin.register(it) } + } + + @FloconMarker + actual override fun getAllDataBases(registeredDatabases: List): List { + val databasesDir = context.context.getDatabasePath("dummy_db") + .parentFile + ?: return emptyList() + + val foundDatabases = mutableListOf() + // Start the recursive search from the base databases directory + scanDirectoryForDatabases( + directory = databasesDir, + depth = 0, + foundDatabases = foundDatabases + ) + + return foundDatabases + } + + private fun scanDirectoryForDatabases( + directory: File, + depth: Int, + foundDatabases: MutableList + ) { + if (depth >= MAX_DEPTH) { + return + } + directory.listFiles()?.forEach { file -> + if (file.isDirectory) { + // If it's a directory, recursively call this function + scanDirectoryForDatabases( + directory = file, + depth = depth + 1, + foundDatabases = foundDatabases, + ) + } else { + // If it's a file, check if it's a database file + if (file.isFile && + !file.name.endsWith("-wal") && // Write-Ahead Log + !file.name.endsWith("-shm") && // Shared-Memory + !file.name.endsWith("-journal") // Older journaling mode + ) { + foundDatabases.add( + FloconFileDatabaseModel( + id = file.absolutePath, // Use absolute path for unique ID + displayName = file.name, + absolutePath = file.absolutePath + ) + ) + } + } + } + } + + companion object { + private const val MAX_DEPTH = 7 + } +} \ No newline at end of file diff --git a/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt b/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt new file mode 100644 index 000000000..fd361d636 --- /dev/null +++ b/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt @@ -0,0 +1,25 @@ +package io.github.openflocon.flocon.database.room3 + +import io.github.openflocon.flocon.database.core.FloconDatabaseConfig +import io.github.openflocon.flocon.dsl.FloconMarker + +class FloconRoom3DatabaseConfig internal constructor() { + internal val paths: MutableList = mutableListOf() + + fun path(path: String) { + paths.add(path) + } + +} + +@OptIn(FloconMarker::class) +fun FloconDatabaseConfig.room(block: FloconRoom3DatabaseConfig.() -> Unit = {}) { + val config = FloconRoom3DatabaseConfig().apply(block) + + providers.add( + FloconRoom3DatabaseProviderImpl( + context = context, + paths = config.paths + ) + ) +} \ No newline at end of file diff --git a/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt b/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt new file mode 100644 index 000000000..b811ac716 --- /dev/null +++ b/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt @@ -0,0 +1,133 @@ +package io.github.openflocon.flocon.database.room3 + +import androidx.room3.RoomDatabase +import androidx.room3.Transactor +import androidx.room3.executeSQL +import androidx.room3.useReaderConnection +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.database.core.databasePlugin +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse + +fun floconRegisterDatabase(displayName: String, database: RoomDatabase) { + Flocon.databasePlugin.register( + FloconRoom3DatabaseModel( + id = displayName, + displayName = displayName, + database = database + ) + ) +} + +fun floconLogDatabaseQuery(databaseName: String, sqlQuery: String, bindArgs: List) { + Flocon.databasePlugin.logQuery( + dbName = databaseName, + sqlQuery = sqlQuery, + bindArgs = bindArgs, + ) +} + +internal data class FloconRoom3DatabaseModel( + override val id: String, + override val displayName: String, + val database: RoomDatabase +) : FloconDatabaseModel { + + override suspend fun executeQuery(query: String): DatabaseExecuteResponse { + return try { + database.useReaderConnection { connection -> + val firstWordUpperCase = getFirstWord(query).uppercase() + + when (firstWordUpperCase) { + "SELECT", + "PRAGMA", + "EXPLAIN" -> executeSelect( + connection = connection, + query = query + ) + + "INSERT" -> executeInsert( + connection = connection, + query = query + ) + + "UPDATE", + "DELETE" -> executeUpdateDelete( + connection = connection, + query = query + ) + + else -> executeRawQuery( + connection = connection, + query = query + ) + } + } + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "error on executeSQL", + originalSql = query, + ) + } + } +} + +private suspend fun executeSelect( + connection: Transactor, + query: String, +): DatabaseExecuteSqlResponse { + return connection.usePrepared(query) { statement -> + val columnNames = mutableListOf() + val columnCount = statement.getColumnCount() + for (i in 0 until columnCount) { + columnNames.add(statement.getColumnName(i)) + } + + val rows = mutableListOf>() + while (statement.step()) { + val values = mutableListOf() + for (i in 0 until columnCount) { + values.add(if (statement.isNull(i)) null else statement.getText(i)) + } + rows.add(values) + } + + DatabaseExecuteSqlResponse.Select( + columns = columnNames, + values = rows + ) + } +} + +private suspend fun executeInsert( + connection: Transactor, + query: String, +): DatabaseExecuteSqlResponse { + connection.executeSQL(query) + val id = connection.usePrepared("SELECT last_insert_rowid()") { it.step(); it.getLong(0) } + return DatabaseExecuteSqlResponse.Insert(id) +} + +private suspend fun executeUpdateDelete( + connection: Transactor, + query: String, +): DatabaseExecuteSqlResponse { + connection.executeSQL(query) + val count = connection.usePrepared("SELECT changes()") { it.step(); it.getLong(0).toInt() } + return DatabaseExecuteSqlResponse.UpdateDelete(count) +} + +private suspend fun executeRawQuery( + connection: Transactor, + query: String, +): DatabaseExecuteSqlResponse { + connection.executeSQL(query) + return DatabaseExecuteSqlResponse.RawSuccess +} + +private fun getFirstWord(s: String): String { + val trimmed = s.trim() + val firstSpace = trimmed.indexOf(' ') + return if (firstSpace >= 0) trimmed.substring(0, firstSpace) else trimmed +} \ No newline at end of file diff --git a/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.kt b/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.kt new file mode 100644 index 000000000..0c5fa278b --- /dev/null +++ b/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.kt @@ -0,0 +1,29 @@ +package io.github.openflocon.flocon.database.room3 + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.databasePlugin +import io.github.openflocon.flocon.database.core.datasource.FloconDatabaseProvider +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.dsl.FloconMarker + +interface FloconRoom3DatabaseProvider : FloconDatabaseProvider { + + fun register() + +} + +@OptIn(FloconMarker::class) +internal expect class FloconRoom3DatabaseProviderImpl( + context: FloconContext, + paths: List +) : FloconRoom3DatabaseProvider { + override fun register() + override fun getAllDataBases(registeredDatabases: List): List +} + +@OptIn(FloconMarker::class) +val Flocon.Companion.databaseRoom3: FloconRoom3DatabaseProvider + get() = databasePlugin.providers + .firstNotNullOfOrNull { it as? FloconRoom3DatabaseProvider } + ?: error("Room3 database provider not initialized") \ No newline at end of file diff --git a/FloconAndroid/database/room3/src/iosMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.ios.kt b/FloconAndroid/database/room3/src/iosMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.ios.kt new file mode 100644 index 000000000..4d0537c54 --- /dev/null +++ b/FloconAndroid/database/room3/src/iosMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.ios.kt @@ -0,0 +1,24 @@ +package io.github.openflocon.flocon.database.room3 + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.databasePlugin +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.dsl.FloconMarker + +@OptIn(markerClass = [FloconMarker::class]) +internal actual class FloconRoom3DatabaseProviderImpl actual constructor( + private val context: FloconContext, + paths: List +) : FloconRoom3DatabaseProvider { + + actual override fun register() { + val databases = getAllDataBases(emptyList()) + databases.forEach { Flocon.databasePlugin.register(it) } + } + + @FloconMarker + actual override fun getAllDataBases(registeredDatabases: List): List { + return emptyList() + } +} diff --git a/FloconAndroid/database/room3/src/jvmMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.jvm.kt b/FloconAndroid/database/room3/src/jvmMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.jvm.kt new file mode 100644 index 000000000..4d0537c54 --- /dev/null +++ b/FloconAndroid/database/room3/src/jvmMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.jvm.kt @@ -0,0 +1,24 @@ +package io.github.openflocon.flocon.database.room3 + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.databasePlugin +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.dsl.FloconMarker + +@OptIn(markerClass = [FloconMarker::class]) +internal actual class FloconRoom3DatabaseProviderImpl actual constructor( + private val context: FloconContext, + paths: List +) : FloconRoom3DatabaseProvider { + + actual override fun register() { + val databases = getAllDataBases(emptyList()) + databases.forEach { Flocon.databasePlugin.register(it) } + } + + @FloconMarker + actual override fun getAllDataBases(registeredDatabases: List): List { + return emptyList() + } +} diff --git a/FloconAndroid/database/room3/src/main/java/io/github/openflocon/flocon/database/room3/extensions/Room3BuilderExt.kt b/FloconAndroid/database/room3/src/main/java/io/github/openflocon/flocon/database/room3/extensions/Room3BuilderExt.kt new file mode 100644 index 000000000..2b0f27d8f --- /dev/null +++ b/FloconAndroid/database/room3/src/main/java/io/github/openflocon/flocon/database/room3/extensions/Room3BuilderExt.kt @@ -0,0 +1,15 @@ +package io.github.openflocon.flocon.database.room3.extensions + +import androidx.room3.RoomDatabase +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +inline fun RoomDatabase.Builder.floconLogs( + name: String? = T::class.simpleName, + executor: Executor = Executors.newSingleThreadExecutor(), + // QueryCallback is removed or altered in Room 3. + queryCallback: Any? = null +): RoomDatabase.Builder { + // TODO: Room 3 removed or changed QueryCallback. Logging must be rewritten for Room 3. + return this +} \ No newline at end of file diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index 3c5e9f5ad..98d34f6df 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -70,6 +70,7 @@ kotlin { // to store the device id implementation("com.russhwolf:multiplatform-settings:1.3.0") + implementation(libs.androidx.sqlite.bundled) } } } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt index a834ef254..946b32851 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconCore.kt @@ -1,5 +1,4 @@ package io.github.openflocon.flocon -expect class FloconContext internal expect fun displayClearTextError(context: FloconContext) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt index ffa1dbc9a..1c7d1ef58 100644 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.plugins.files import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.plugins.files.model.fromdevice.FileDataModel internal actual fun fileDataSource(context: FloconContext): FileDataSource { @@ -9,6 +10,7 @@ internal actual fun fileDataSource(context: FloconContext): FileDataSource { } // TODO +@io.github.openflocon.flocon.dsl.FloconMarker internal class FileDataSourceIOs : FileDataSource { override fun getFile( path: String, diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.ios.kt index cd9fa5333..690d060fa 100644 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.ios.kt +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.ios.kt @@ -13,10 +13,13 @@ import io.ktor.http.HttpHeaders import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json +import io.github.openflocon.flocon.dsl.FloconMarker + internal actual fun buildFloconHttpClient(): FloconHttpClient { return FloconHttpClientIOs() } +@io.github.openflocon.flocon.dsl.FloconMarker internal class FloconHttpClientIOs() : FloconHttpClient { // client configurable selon la plateforme (Android, iOS, JVM, etc.) diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index f65bafb13..c8aafb267 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -93,6 +93,9 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = " androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" } +androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version = "3.0.0-alpha01" } +androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version = "3.0.0-alpha01" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } androidx-room = { id = "androidx.room", version.ref = "room" } diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index e136cdea7..e16ed5784 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -90,6 +90,8 @@ dependencies { debugImplementation(project(":database:room")) releaseImplementation(project(":database:room-no-op")) + debugImplementation(project(":database:room3")) + releaseImplementation(project(":database:room3-no-op")) debugImplementation(project(":network:okhttp-interceptor")) releaseImplementation(project(":network:okhttp-interceptor-no-op")) diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index 1a9e42680..748d46048 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -37,3 +37,5 @@ include(":database:core") include(":database:core-no-op") include(":database:room") include(":database:room-no-op") +include(":database:room3") +include(":database:room3-no-op") From e826685568230b2618c4b4e760899ed7a8fd009a Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Sun, 5 Apr 2026 22:04:31 +0200 Subject: [PATCH 20/38] fix: Build --- .../deeplinks/FloconDeeplinkEncoding.kt | 18 +++++++ .../plugins/deeplinks/FloconDeeplinks.kt | 23 +++++++-- .../deeplinks/FloconDeeplinksConfig.kt | 37 ++++++++++++++ .../deeplinks/FloconDeeplinksPlugin.kt | 49 ++----------------- .../flocon/plugins/deeplinks/Mapping.kt | 39 ++++++++++++--- .../plugins/deeplinks/model/DeeplinkModel.kt | 24 ++++++--- .../deeplinks/model/DeeplinksRemote.kt | 47 +++++++++++++++--- .../openflocon/flocon/core/FloconEncoder.kt | 8 +-- .../flocon/myapplication/MainActivity.kt | 19 ++++--- 9 files changed, 179 insertions(+), 85 deletions(-) create mode 100644 FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinkEncoding.kt create mode 100644 FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksConfig.kt diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinkEncoding.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinkEncoding.kt new file mode 100644 index 000000000..45f898644 --- /dev/null +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinkEncoding.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocon.plugins.deeplinks + +import io.github.openflocon.flocon.FloconEncoding +import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass + +@FloconMarker +internal class FloconDeeplinkEncoding : FloconEncoding { + override val serializersModule: SerializersModule = SerializersModule { + polymorphic(DeeplinkParameterRemote::class) { + subclass(DeeplinkParameterRemote.AutoComplete::class) + subclass(DeeplinkParameterRemote.Variable::class) + } + } +} \ No newline at end of file diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index 6caacd52c..83c8ef103 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -2,6 +2,7 @@ package io.github.openflocon.flocon.plugins.deeplinks import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconEncoding import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginFactory @@ -13,7 +14,11 @@ import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel object FloconDeeplinks : FloconPluginFactory { override val name: String = "Deeplinks" override val pluginId: String = FloconDeeplinks::class.simpleName!! - override fun createConfig(context: FloconContext) = FloconDeeplinksConfig() + override fun createConfig(context: FloconContext): FloconDeeplinksConfig = + FloconDeeplinksConfigImpl() + + @FloconMarker + override fun createEncoding(): FloconEncoding = FloconDeeplinkEncoding() @OptIn(FloconMarker::class) override fun install( @@ -21,7 +26,8 @@ object FloconDeeplinks : FloconPluginFactory, + private val variables: List, private val sender: FloconMessageSender, ) : FloconPlugin, FloconDeeplinksPlugin { override val key: String = "DEEP_LINK" @@ -43,15 +50,21 @@ internal class FloconDeeplinksPluginImpl( } override suspend fun onConnectedToServer() { - registerDeeplinks(deeplinks) + registerDeeplinks( + deeplinks = deeplinks, + variables = variables + ) } - override suspend fun registerDeeplinks(deeplinks: List) { + suspend fun registerDeeplinks( + deeplinks: List, + variables: List + ) { try { sender.send( plugin = Protocol.FromDevice.Deeplink.Plugin, method = Protocol.FromDevice.Deeplink.Method.GetDeeplinks, - body = toDeeplinksJson(deeplinks) + body = createJson(deeplinks = deeplinks, variables = variables) ) } catch (t: Throwable) { t.printStackTrace() diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksConfig.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksConfig.kt new file mode 100644 index 000000000..7dd0e694c --- /dev/null +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksConfig.kt @@ -0,0 +1,37 @@ +package io.github.openflocon.flocon.plugins.deeplinks + +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel + +abstract class FloconDeeplinksConfig : FloconPluginConfig { + abstract fun variable(name: String, block: DeeplinkVariableBuilder.() -> Unit = {}) + + abstract fun deeplink(link: String, block: DeeplinkLinkBuilder.() -> Unit = {}) + + internal abstract fun deeplinks(): List + internal abstract fun variables(): List +} + +internal class FloconDeeplinksConfigImpl internal constructor() : FloconDeeplinksConfig() { + + private val variables = mutableListOf() + private val deeplinks = mutableListOf() + + override fun variable(name: String, block: DeeplinkVariableBuilder.() -> Unit) { + val variable = DeeplinkVariableBuilder(name).apply(block) + .build() + + variables.add(variable) + } + + override fun deeplink(link: String, block: DeeplinkLinkBuilder.() -> Unit) { + val deeplink = DeeplinkLinkBuilder(link).apply(block) + .build() + + deeplinks.add(deeplink) + } + + override fun deeplinks(): List = deeplinks.toList() + override fun variables(): List = variables.toList() + +} \ No newline at end of file diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index 337106b29..2369b1f7f 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -1,7 +1,6 @@ package io.github.openflocon.flocon.plugins.deeplinks import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel class DeeplinkLinkBuilder internal constructor( @@ -14,14 +13,14 @@ class DeeplinkLinkBuilder internal constructor( infix fun String.withAutoComplete(suggestions: List) { parameters[this] = DeeplinkModel.Parameter.AutoComplete( - paramName = this, - suggestions.distinct() + name = this, + autoComplete = suggestions.distinct() ) } infix fun String.withVariable(variableName: String) { parameters[this] = DeeplinkModel.Parameter.Variable( - paramName = this, + name = this, variableName = variableName ) } @@ -70,44 +69,4 @@ data class DeeplinkVariable( } -class DeeplinkBuilder { - private val variables = mutableListOf() - private val deeplinks = mutableListOf() - - fun variable(name: String, block: DeeplinkVariableBuilder.() -> Unit = {}) { - val variable = DeeplinkVariableBuilder(name).apply(block) - .build() - - variables.add(variable) - } - - fun deeplink(link: String, block: DeeplinkLinkBuilder.() -> Unit = {}) { - val deeplink = DeeplinkLinkBuilder(link).apply(block) - .build() - - deeplinks.add(deeplink) - } - - internal fun deeplinks(): List = deeplinks.toList() - internal fun variables(): List = variables.toList() -} - -fun FloconApp.deeplinks(deeplinksBlock: DeeplinkBuilder.() -> Unit) { - this.client?.deeplinksPlugin?.let { - val builder = DeeplinkBuilder().apply(deeplinksBlock) - - it.registerDeeplinks( - deeplinks = builder.deeplinks(), - variables = builder.variables() - ) - } -} - -interface FloconDeeplinksPlugin { - - fun registerDeeplinks( - deeplinks: List, - variables: List - ) - -} \ No newline at end of file +interface FloconDeeplinksPlugin : FloconPlugin \ No newline at end of file diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt index dd57bb442..02627c180 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt @@ -4,11 +4,18 @@ import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkRemote +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkVariableRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinksRemote -import kotlinx.serialization.encodeToString -internal fun toDeeplinksJson(deeplinks: List): String { - val dto = DeeplinksRemote(deeplinks.map { it.toRemote() }) +internal fun createJson( + deeplinks: List, + variables: List +): String { + val dto = DeeplinksRemote( + deeplinks = deeplinks.map(DeeplinkModel::toRemote), + variables = variables.map(DeeplinkVariable::toRemote) + ) + return FloconEncoder.json.encodeToString(dto) } @@ -19,7 +26,27 @@ internal fun DeeplinkModel.toRemote(): DeeplinkRemote = DeeplinkRemote( parameters = parameters.map { it.toRemote() } ) -internal fun DeeplinkModel.Parameter.toRemote(): DeeplinkParameterRemote = DeeplinkParameterRemote( - paramName = paramName, - autoComplete = autoComplete +internal fun DeeplinkModel.Parameter.toRemote(): DeeplinkParameterRemote = when (this) { + is DeeplinkModel.Parameter.AutoComplete -> DeeplinkParameterRemote.AutoComplete( + name = name, + autoComplete = autoComplete + ) + + is DeeplinkModel.Parameter.Variable -> DeeplinkParameterRemote.Variable( + name = name, + variableName = variableName + ) +} + +internal fun DeeplinkVariable.toRemote() = DeeplinkVariableRemote( + name = name, + mode = mode.toRemote() ) + +internal fun DeeplinkVariable.Mode.toRemote() = when (this) { + is DeeplinkVariable.Mode.AutoComplete -> DeeplinkVariableRemote.Mode.AutoComplete( + suggestions = suggestions + ) + + DeeplinkVariable.Mode.Input -> DeeplinkVariableRemote.Mode.Input +} \ No newline at end of file diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt index bf521ba14..1578513e1 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt @@ -1,13 +1,25 @@ package io.github.openflocon.flocon.plugins.deeplinks.model -data class DeeplinkModel( +@ConsistentCopyVisibility +data class DeeplinkModel internal constructor( val link: String, val label: String? = null, val description: String? = null, - val parameters: List, + val parameters: List ) { - data class Parameter( - val paramName: String, - val autoComplete: List, - ) + sealed interface Parameter { + val name: String + + @ConsistentCopyVisibility + data class AutoComplete internal constructor( + override val name: String, + val autoComplete: List + ) : Parameter + + @ConsistentCopyVisibility + data class Variable internal constructor( + override val name: String, + val variableName: String + ) : Parameter + } } diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt index 06cd4cf38..1c9e7a37e 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt @@ -1,12 +1,8 @@ package io.github.openflocon.flocon.plugins.deeplinks.model +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable - -@Serializable -internal class DeeplinkParameterRemote( - val paramName: String, - val autoComplete: List, -) +import kotlinx.serialization.json.JsonClassDiscriminator @Serializable internal class DeeplinkRemote( @@ -19,4 +15,43 @@ internal class DeeplinkRemote( @Serializable internal class DeeplinksRemote( val deeplinks: List, + val variables: List ) + +@Serializable +internal data class DeeplinkVariableRemote( + val name: String, + val mode: Mode = Mode.Input, + val description: String? = null +) { + + @Serializable + @JsonClassDiscriminator("mode") + sealed interface Mode { + @SerialName("input") + object Input : Mode + + @SerialName("auto_complete") + data class AutoComplete(val suggestions: List) : Mode + } + +} + +@JsonClassDiscriminator("type") +internal sealed interface DeeplinkParameterRemote { + val name: String + + @Serializable + @SerialName("auto_complete") + data class AutoComplete( + override val name: String, + val autoComplete: List + ) : DeeplinkParameterRemote + + @Serializable + @SerialName("variable") + data class Variable( + override val name: String, + val variableName: String + ) : DeeplinkParameterRemote +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt index 679355f17..cf5659731 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt @@ -1,10 +1,7 @@ package io.github.openflocon.flocon.core -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.polymorphic -import kotlinx.serialization.modules.subclass object FloconEncoder { val json = Json { @@ -13,10 +10,7 @@ object FloconEncoder { encodeDefaults = false serializersModule = SerializersModule { - polymorphic(DeeplinkParameterRemote::class) { - subclass(DeeplinkParameterRemote.AutoComplete::class) - subclass(DeeplinkParameterRemote.Variable::class) - } + } } } \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index 237bbafa5..acc9d0f03 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -221,19 +221,18 @@ class MainActivity : ComponentActivity() { private fun initFlocon() { startFlocon(FloconContext(this)) { install(FloconDeeplinks) { - register("flocon://home") - register("flocon://test") - register( - "flocon://user/[userId]", - label = "User" + deeplink("flocon://home") + deeplink("flocon://test") + deeplink( + "flocon://user/[userId]" ) { - param("userId", listOf("Florent", "David", "Guillaume")) + label = "User" + "userId" withAutoComplete listOf("Florent", "David", "Guillaume") } - register( - "flocon://post/[postId]?comment=[commentText]", - label = "Post", + deeplink("flocon://post/[postId]?comment=[commentText]") { + label = "Post" description = "Open a post and send a comment" - ) + } } install(FloconNetwork) From 9bda13e518f3dc8ae257721745d8f788945d01e1 Mon Sep 17 00:00:00 2001 From: Raphael Teyssandier Date: Sun, 5 Apr 2026 22:14:34 +0200 Subject: [PATCH 21/38] 2.0.0 - Rework gradle (#510) --- .../build-logic/convention/build.gradle.kts | 28 +++ .../openflocon/buildlogic/AndroidConfig.kt | 22 ++ .../FloconAndroidLibraryConventionPlugin.kt | 26 ++ ...oconKotlinMultiplatformConventionPlugin.kt | 38 +++ .../FloconPublishConventionPlugin.kt | 52 ++++ FloconAndroid/build-logic/gradle.properties | 2 + FloconAndroid/build-logic/settings.gradle.kts | 15 ++ FloconAndroid/build.gradle.kts | 2 +- .../database/core-no-op/build.gradle.kts | 81 +----- FloconAndroid/database/core/build.gradle.kts | 81 +----- .../database/room-no-op/build.gradle.kts | 67 +---- FloconAndroid/database/room/build.gradle.kts | 73 +----- .../database/room3-no-op/build.gradle.kts | 19 +- FloconAndroid/database/room3/build.gradle.kts | 10 +- .../datastores-no-op/build.gradle.kts | 123 +++------ FloconAndroid/datastores/build.gradle.kts | 124 +++------ .../deeplinks-no-op/build.gradle.kts | 84 +------ FloconAndroid/deeplinks/build.gradle.kts | 81 +----- .../flocon/plugins/deeplinks/Mapping.kt | 1 + FloconAndroid/flocon-no-op/build.gradle.kts | 80 +----- FloconAndroid/flocon/build.gradle.kts | 236 ++++++------------ FloconAndroid/gradle/libs.versions.toml | 42 ++-- .../grpc-interceptor-base/build.gradle.kts | 123 +++------ .../grpc-interceptor-lite/build.gradle.kts | 117 ++------- .../grpc/grpc-interceptor/build.gradle.kts | 118 ++------- .../network/core-no-op/build.gradle.kts | 81 +----- FloconAndroid/network/core/build.gradle.kts | 82 +----- .../ktor-interceptor-no-op/build.gradle.kts | 82 +----- .../network/ktor-interceptor/build.gradle.kts | 86 +------ .../okhttp-interceptor-no-op/build.gradle.kts | 113 ++------- .../okhttp-interceptor/build.gradle.kts | 130 +++------- .../sample-android-only/build.gradle.kts | 30 +-- .../sample-multiplatform/build.gradle.kts | 4 +- FloconAndroid/settings.gradle.kts | 8 +- 34 files changed, 604 insertions(+), 1657 deletions(-) create mode 100644 FloconAndroid/build-logic/convention/build.gradle.kts create mode 100644 FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/AndroidConfig.kt create mode 100644 FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconAndroidLibraryConventionPlugin.kt create mode 100644 FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconKotlinMultiplatformConventionPlugin.kt create mode 100644 FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconPublishConventionPlugin.kt create mode 100644 FloconAndroid/build-logic/gradle.properties create mode 100644 FloconAndroid/build-logic/settings.gradle.kts diff --git a/FloconAndroid/build-logic/convention/build.gradle.kts b/FloconAndroid/build-logic/convention/build.gradle.kts new file mode 100644 index 000000000..82674d015 --- /dev/null +++ b/FloconAndroid/build-logic/convention/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `kotlin-dsl` +} + +group = "io.github.openflocon.buildlogic" + +dependencies { + implementation(libs.android.gradlePlugin) + implementation(libs.kotlin.gradlePlugin) + implementation(libs.vanniktech.mavenPublish.gradlePlugin) +} + +gradlePlugin { + plugins { + register("floconAndroidLibrary") { + id = "flocon.android.library" + implementationClass = "io.github.openflocon.buildlogic.FloconAndroidLibraryConventionPlugin" + } + register("floconKotlinMultiplatform") { + id = "flocon.kotlin.multiplatform" + implementationClass = "io.github.openflocon.buildlogic.FloconKotlinMultiplatformConventionPlugin" + } + register("floconPublish") { + id = "flocon.publish" + implementationClass = "io.github.openflocon.buildlogic.FloconPublishConventionPlugin" + } + } +} diff --git a/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/AndroidConfig.kt b/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/AndroidConfig.kt new file mode 100644 index 000000000..e25d4d737 --- /dev/null +++ b/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/AndroidConfig.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.buildlogic + +import com.android.build.gradle.LibraryExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +internal fun Project.configureAndroidLibrary() { + extensions.configure { + compileSdk = 36 + + defaultConfig { + minSdk = 23 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + } +} diff --git a/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconAndroidLibraryConventionPlugin.kt b/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconAndroidLibraryConventionPlugin.kt new file mode 100644 index 000000000..af1fb9a27 --- /dev/null +++ b/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconAndroidLibraryConventionPlugin.kt @@ -0,0 +1,26 @@ +package io.github.openflocon.buildlogic + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +class FloconAndroidLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + } + + configureAndroidLibrary() + + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + } + } +} diff --git a/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconKotlinMultiplatformConventionPlugin.kt b/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconKotlinMultiplatformConventionPlugin.kt new file mode 100644 index 000000000..f60e19246 --- /dev/null +++ b/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconKotlinMultiplatformConventionPlugin.kt @@ -0,0 +1,38 @@ +package io.github.openflocon.buildlogic + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +class FloconKotlinMultiplatformConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("org.jetbrains.kotlin.multiplatform") + apply("com.android.library") + } + + configureAndroidLibrary() + + extensions.configure { + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + compilerOptions { + freeCompilerArgs.add("-XXLanguage:+ExpectRefinement") + } + } + } + } +} diff --git a/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconPublishConventionPlugin.kt b/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconPublishConventionPlugin.kt new file mode 100644 index 000000000..df47b1eaf --- /dev/null +++ b/FloconAndroid/build-logic/convention/src/main/kotlin/io/github/openflocon/buildlogic/FloconPublishConventionPlugin.kt @@ -0,0 +1,52 @@ +package io.github.openflocon.buildlogic + +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class FloconPublishConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.vanniktech.maven.publish") + } + + extensions.configure { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + pom { + name.set(project.name) + description.set(project.findProperty("floconDescription") as? String) + inceptionYear.set("2025") + url.set("https://github.com/openflocon/Flocon") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + id.set("openflocon") + name.set("Open Flocon") + url.set("https://github.com/openflocon") + } + } + scm { + url.set("https://github.com/openflocon/Flocon") + connection.set("scm:git:git://github.com/openflocon/Flocon.git") + developerConnection.set("scm:git:ssh://git@github.com/openflocon/Flocon.git") + } + } + } + } + } +} diff --git a/FloconAndroid/build-logic/gradle.properties b/FloconAndroid/build-logic/gradle.properties new file mode 100644 index 000000000..b29124d09 --- /dev/null +++ b/FloconAndroid/build-logic/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +kotlin.daemon.jvmargs=-Xmx1024m diff --git a/FloconAndroid/build-logic/settings.gradle.kts b/FloconAndroid/build-logic/settings.gradle.kts new file mode 100644 index 000000000..f26e6d9f9 --- /dev/null +++ b/FloconAndroid/build-logic/settings.gradle.kts @@ -0,0 +1,15 @@ +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" + +include(":convention") diff --git a/FloconAndroid/build.gradle.kts b/FloconAndroid/build.gradle.kts index 5d6f07e57..8cc104597 100644 --- a/FloconAndroid/build.gradle.kts +++ b/FloconAndroid/build.gradle.kts @@ -7,5 +7,5 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.vanniktech.maven.publish) apply false - id("com.google.protobuf") version "0.9.5" apply false + alias(libs.plugins.protobuf) apply false } \ No newline at end of file diff --git a/FloconAndroid/database/core-no-op/build.gradle.kts b/FloconAndroid/database/core-no-op/build.gradle.kts index b7390b113..348ce3da1 100644 --- a/FloconAndroid/database/core-no-op/build.gradle.kts +++ b/FloconAndroid/database/core-no-op/build.gradle.kts @@ -1,29 +1,14 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(projects.flocon) + implementation(libs.kotlinx.coroutines.core) } } @@ -51,68 +36,14 @@ kotlin { android { namespace = "io.github.openflocon.flocon.database.core.noop" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-database-core-no-op", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Database Core No-Op" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } + diff --git a/FloconAndroid/database/core/build.gradle.kts b/FloconAndroid/database/core/build.gradle.kts index f66e064ad..346a4e2b9 100644 --- a/FloconAndroid/database/core/build.gradle.kts +++ b/FloconAndroid/database/core/build.gradle.kts @@ -1,31 +1,16 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") alias(libs.plugins.kotlin.serialization) } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - api(project(":flocon")) + api(projects.flocon) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) } } @@ -58,68 +43,14 @@ kotlin { android { namespace = "io.github.openflocon.flocon.database.core" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-database-core", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Database Core" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } + diff --git a/FloconAndroid/database/room-no-op/build.gradle.kts b/FloconAndroid/database/room-no-op/build.gradle.kts index ffd49f7e0..22b4f41d1 100644 --- a/FloconAndroid/database/room-no-op/build.gradle.kts +++ b/FloconAndroid/database/room-no-op/build.gradle.kts @@ -1,28 +1,13 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - implementation(project(":database:core-no-op")) + implementation(projects.database.coreNoOp) } } @@ -50,57 +35,15 @@ kotlin { android { namespace = "io.github.openflocon.flocon.database.room.noop" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-database-room-no-op", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Room Implementation No-Op" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } + diff --git a/FloconAndroid/database/room/build.gradle.kts b/FloconAndroid/database/room/build.gradle.kts index 4b8d7eb40..eb82a4a12 100644 --- a/FloconAndroid/database/room/build.gradle.kts +++ b/FloconAndroid/database/room/build.gradle.kts @@ -1,29 +1,14 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) - api(project(":database:core")) + implementation(projects.flocon) + api(projects.database.core) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.sqlite.wrapper) implementation(libs.androidx.sqlite.bundled) @@ -32,7 +17,7 @@ kotlin { val androidMain by getting { dependencies { - implementation(libs.jetbrains.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.android) } } @@ -55,59 +40,15 @@ kotlin { android { namespace = "io.github.openflocon.flocon.database.room" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-database-room", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Room implementation" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } + diff --git a/FloconAndroid/database/room3-no-op/build.gradle.kts b/FloconAndroid/database/room3-no-op/build.gradle.kts index 5c3db1e82..470e7406c 100644 --- a/FloconAndroid/database/room3-no-op/build.gradle.kts +++ b/FloconAndroid/database/room3-no-op/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) @@ -6,10 +8,8 @@ plugins { kotlin { androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } @@ -56,6 +56,16 @@ android { minSdk = 23 } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -78,6 +88,7 @@ mavenPublishing { version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) + pom { name = "Flocon Room 3 Implementation No-Op" description = project.property("floconDescription") as String diff --git a/FloconAndroid/database/room3/build.gradle.kts b/FloconAndroid/database/room3/build.gradle.kts index 99e8d69c1..cf12f31f2 100644 --- a/FloconAndroid/database/room3/build.gradle.kts +++ b/FloconAndroid/database/room3/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) @@ -8,10 +10,8 @@ plugins { kotlin { androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } @@ -32,11 +32,13 @@ kotlin { val androidMain by getting { dependencies { + implementation(libs.brotli.dec) } } val jvmMain by getting { dependencies { + implementation(libs.brotli.dec) } } diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index c3f4b30d0..d89542b0c 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -1,93 +1,30 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" -} - -android { - namespace = "io.github.openflocon.flocon.datastores" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -dependencies { - - implementation(project(":flocon")) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.jetbrains.kotlinx.coroutines.core) - - implementation(libs.androidx.datastore.preferences) -} - - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-datastores-no-op", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - pom { - name = "Flocon Datastores Integration No Op" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.android.library") + id("flocon.publish") +} + +android { + namespace = "io.github.openflocon.flocon.datastores" +} + + +dependencies { + + implementation(projects.flocon) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.androidx.datastore.preferences) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-datastores-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index e30b81eca..6974dfad8 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -1,94 +1,30 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" -} - -android { - namespace = "io.github.openflocon.flocon.datastores" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -dependencies { - - implementation(project(":flocon")) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.jetbrains.kotlinx.coroutines.core) - implementation(libs.jetbrains.kotlinx.coroutines.android) - - implementation(libs.androidx.datastore.preferences) -} - - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-datastores", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - pom { - name = "Flocon Datastores Integration" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.android.library") + id("flocon.publish") +} + +android { + namespace = "io.github.openflocon.flocon.datastores" +} + +dependencies { + + implementation(projects.flocon) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + implementation(libs.androidx.datastore.preferences) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-datastores", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/deeplinks-no-op/build.gradle.kts b/FloconAndroid/deeplinks-no-op/build.gradle.kts index b183bb71e..438e43cdb 100644 --- a/FloconAndroid/deeplinks-no-op/build.gradle.kts +++ b/FloconAndroid/deeplinks-no-op/build.gradle.kts @@ -1,37 +1,22 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(projects.flocon) + implementation(libs.kotlinx.coroutines.core) } } - + val androidMain by getting { dependencies { } } - + val jvmMain by getting { dependencies { } @@ -51,68 +36,13 @@ kotlin { android { namespace = "io.github.openflocon.flocon.deeplinks.noop" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-deeplinks-no-op", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Deeplinks No-Op" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } diff --git a/FloconAndroid/deeplinks/build.gradle.kts b/FloconAndroid/deeplinks/build.gradle.kts index 25714f431..0049a7aef 100644 --- a/FloconAndroid/deeplinks/build.gradle.kts +++ b/FloconAndroid/deeplinks/build.gradle.kts @@ -1,30 +1,15 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") alias(libs.plugins.kotlin.serialization) } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(projects.flocon) + implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) } } @@ -53,68 +38,14 @@ kotlin { android { namespace = "io.github.openflocon.flocon.deeplinks" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-deeplinks", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Deeplinks" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } + diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt index 02627c180..89caedf6c 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt @@ -6,6 +6,7 @@ import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemo import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkVariableRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinksRemote +import kotlinx.serialization.encodeToString internal fun createJson( deeplinks: List, diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index 895bd36bf..99ab4b8c7 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -1,29 +1,16 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) - api(project(":flocon")) + api(projects.flocon) + implementation(libs.kotlinx.coroutines.core) } } @@ -51,68 +38,13 @@ kotlin { android { namespace = "io.github.openflocon.flocon" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-no-op", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon No Op" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +} diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index 98d34f6df..ab063ba78 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -1,154 +1,82 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.vanniktech.maven.publish) - alias(libs.plugins.buildconfig) -} - -kotlin { - androidTarget { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - - compilerOptions { - freeCompilerArgs.add("-XXLanguage:+ExpectRefinement") - } - - sourceSets { - val commonMain by getting { - dependencies { - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) - implementation(libs.kotlinx.serialization.json) - } - } - - val androidMain by getting { - dependencies { - implementation(libs.kotlinx.coroutines.android) - implementation(libs.jakewharton.process.phoenix) - implementation("com.squareup.okhttp3:okhttp:4.12.0") - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.cio) - - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.client.logging) - implementation(libs.ktor.serialization.kotlinx.json) - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.darwin) - - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.client.logging) - implementation(libs.ktor.serialization.kotlinx.json) - - // to store the device id - implementation("com.russhwolf:multiplatform-settings:1.3.0") - implementation(libs.androidx.sqlite.bundled) - } - } - } -} - -buildConfig { - packageName("io.github.openflocon.flocondesktop") - - buildConfigField("APP_VERSION", System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String) -} - -android { - namespace = "io.github.openflocon.flocon" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - sourceSets["main"].res.srcDirs("src/androidMain/res") -} - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - pom { - name = "Flocon" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.kotlin.multiplatform") + alias(libs.plugins.kotlin.serialization) + id("flocon.publish") + alias(libs.plugins.buildconfig) +} + + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.kotlinx.coroutines.android) + implementation(libs.jakewharton.process.phoenix) + implementation("com.squareup.okhttp3:okhttp:4.12.0") + } + } + + val jvmMain by getting { + dependencies { + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.serialization.kotlinx.json) + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + dependencies { + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.darwin) + + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.serialization.kotlinx.json) + + // to store the device id + implementation("com.russhwolf:multiplatform-settings:1.3.0") + implementation(libs.androidx.sqlite.bundled) + } + } + } +} + +buildConfig { + packageName("io.github.openflocon.flocondesktop") + + buildConfigField("APP_VERSION", System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String) +} + +android { + namespace = "io.github.openflocon.flocon" + + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].res.srcDirs("src/androidMain/res") +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index c8aafb267..e0c4d0368 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.11.0-rc02" +agp = "8.13.2" apollo = "4.0.0" coilCompose = "3.2.0" compose = "1.9.0" @@ -10,19 +10,19 @@ coreKtx = "1.16.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" -kotlinxCoroutinesBom = "1.10.2" -kotlinxSerialization = "1.8.0" +kotlinxCoroutines = "1.10.2" +kotlinxSerialization = "1.7.1" ktor = "3.2.3" lifecycleRuntimeKtx = "2.9.1" activityCompose = "1.10.1" composeBom = "2025.06.01" appcompat = "1.7.1" material = "1.12.0" -okhttpBom = "4.12.0" +okhttp = "4.12.0" room = "2.8.4" # for grpc gson = "2.11.0" -grpc = "1.70.0" +grpc = "1.73.0" protobufPlugin = "0.9.5" grpcKotlin = "1.4.3" protobuf = "4.26.1" @@ -34,6 +34,9 @@ buildconfig = "5.6.8" brotli = "0.1.2" [libraries] +android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } +kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +vanniktech-mavenPublish-gradlePlugin = { group = "com.vanniktech", name = "gradle-maven-publish-plugin", version.ref = "mavenPublish" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } brotli-dec = { module = "org.brotli:dec", version.ref = "brotli" } @@ -43,9 +46,6 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCo coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coilCompose" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" } jakewharton-process-phoenix = { group = "com.jakewharton", name = "process-phoenix", version.ref = "processPhoenix" } -jetbrains-kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } -jetbrains-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } -jetbrains-kotlinx-coroutines-core-fixed = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesBom" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -59,17 +59,22 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } + # for grpc gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } grpc-android = { group = "io.grpc", name = "grpc-android", version.ref = "grpc" } grpc-okhttp = { group = "io.grpc", name = "grpc-okhttp", version.ref = "grpc" } grpc-kotlin-stub = { group = "io.grpc", name = "grpc-kotlin-stub", version.ref = "grpcKotlin" } grpc-protobuf-lite = { group = "io.grpc", name = "grpc-protobuf-lite", version.ref = "grpc" } -kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } -kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinxCoroutinesBom" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } -kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesBom" } +grpc-gen-java = { group = "io.grpc", name = "protoc-gen-grpc-java", version.ref = "grpc" } +grpc-gen-kotlin = { group = "io.grpc", name = "protoc-gen-grpc-kotlin", version.ref = "grpcKotlin" } + +kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } + ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } @@ -78,15 +83,15 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-clientJava = { module = "io.ktor:ktor-client-java", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -okhttp = { module = "com.squareup.okhttp3:okhttp" } -okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" } -okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp" } -org-jetbrains-kotlinx-kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } -org-jetbrains-kotlinx-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } + +okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } + +protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version = "3.25.1" } protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf"} protobuf-util = { group = "com.google.protobuf", name = "protobuf-java-util", version.ref = "protobuf" } + sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" } -squareup-okhttp = { module = "com.squareup.okhttp3:okhttp" } androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } @@ -109,3 +114,4 @@ android-library = { id = "com.android.library", version.ref = "agp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildconfig" } +protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } diff --git a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts index 4e00098c9..fb3e112e3 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts @@ -1,93 +1,30 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" -} - -android { - namespace = "io.github.openflocon.flocon.grpc.base" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -dependencies { - implementation(project(":flocon")) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.kotlinx.coroutines.core) - - implementation(libs.grpc.android) -} - - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-grpc-interceptor-base", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - - pom { - name = "Flocon Grpc Interceptor" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.android.library") + id("flocon.publish") +} + + +android { + namespace = "io.github.openflocon.flocon.grpc.base" +} + + +dependencies { + implementation(projects.flocon) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.grpc.android) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-grpc-interceptor-base", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts index cc99dcd78..9f49224d7 100644 --- a/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts @@ -1,90 +1,27 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" -} - -android { - namespace = "io.github.openflocon.flocon.grpc.lite" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -dependencies { - api(project(":grpc:grpc-interceptor-base")) - - implementation(libs.grpc.android) - implementation(libs.gson) -} - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-grpc-interceptor-lite", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - - pom { - name = "Flocon Grpc Interceptor Lite" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.android.library") + id("flocon.publish") +} + + +android { + namespace = "io.github.openflocon.flocon.grpc.lite" +} + + +dependencies { + api(projects.grpc.grpcInterceptorBase) + + implementation(libs.grpc.android) + implementation(libs.gson) +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-grpc-interceptor-lite", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts index c39e77e0f..ef4b916ff 100644 --- a/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts @@ -1,91 +1,27 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" -} - -android { - namespace = "io.github.openflocon.flocon.grpc" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -dependencies { - api(project(":grpc:grpc-interceptor-base")) - - implementation(libs.grpc.android) - implementation(libs.protobuf.util) -} - - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-grpc-interceptor", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - - pom { - name = "Flocon Grpc Interceptor" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.android.library") + id("flocon.publish") +} + +android { + namespace = "io.github.openflocon.flocon.grpc" +} + + +dependencies { + api(projects.grpc.grpcInterceptorBase) + + implementation(libs.grpc.android) + implementation(libs.protobuf.util) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-grpc-interceptor", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/build.gradle.kts b/FloconAndroid/network/core-no-op/build.gradle.kts index 7d09e14e2..27fa6b6cf 100644 --- a/FloconAndroid/network/core-no-op/build.gradle.kts +++ b/FloconAndroid/network/core-no-op/build.gradle.kts @@ -1,29 +1,14 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(projects.flocon) + implementation(libs.kotlinx.coroutines.core) } } @@ -51,68 +36,14 @@ kotlin { android { namespace = "io.github.openflocon.flocon.network.core.noop" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-network-core-no-op", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Network Core No-Op" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } + diff --git a/FloconAndroid/network/core/build.gradle.kts b/FloconAndroid/network/core/build.gradle.kts index af58159d7..a677713ae 100644 --- a/FloconAndroid/network/core/build.gradle.kts +++ b/FloconAndroid/network/core/build.gradle.kts @@ -1,31 +1,15 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") alias(libs.plugins.kotlin.serialization) } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - api(project(":flocon")) - - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + api(projects.flocon) + implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) } } @@ -54,68 +38,14 @@ kotlin { android { namespace = "io.github.openflocon.flocon.network.core" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-network-core", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Network Core" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } + diff --git a/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts index 0f966ba28..821e8a0a5 100644 --- a/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts @@ -1,28 +1,13 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - implementation(project(":network:core-no-op")) + implementation(projects.network.coreNoOp) implementation(libs.ktor.client.core) } } @@ -51,71 +36,14 @@ kotlin { android { namespace = "io.github.openflocon.flocon.ktor" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-ktor-interceptor-no-op", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - - pom { - name = "Flocon Ktor Interceptor No Op" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +} diff --git a/FloconAndroid/network/ktor-interceptor/build.gradle.kts b/FloconAndroid/network/ktor-interceptor/build.gradle.kts index 2404e2b0b..57fd88634 100644 --- a/FloconAndroid/network/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor/build.gradle.kts @@ -1,32 +1,17 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { - api(project(":flocon")) - api(project(":network:core")) + api(projects.flocon) + api(projects.network.core) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") + implementation(libs.kotlinx.coroutines.core) implementation(libs.ktor.client.core) } } @@ -57,71 +42,14 @@ kotlin { android { namespace = "io.github.openflocon.flocon.ktor" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } +mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-ktor-interceptor", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - - pom { - name = "Flocon Ktor Interceptor" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +} diff --git a/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts b/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts index 739f6b1ee..aa77a7815 100644 --- a/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts @@ -1,88 +1,25 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" -} - -android { - namespace = "io.github.openflocon.flocon.okhttp" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -dependencies { - implementation(project(":network:core-no-op")) - implementation(platform(libs.okhttp.bom)) - implementation(libs.okhttp3.okhttp) -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-okhttp-interceptor-no-op", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - pom { - name = "Flocon OkHttp Interceptor" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +plugins { + id("flocon.android.library") + id("flocon.publish") +} + + +android { + namespace = "io.github.openflocon.flocon.okhttp" +} + + +dependencies { + implementation(projects.network.coreNoOp) + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-okhttp-interceptor-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/network/okhttp-interceptor/build.gradle.kts b/FloconAndroid/network/okhttp-interceptor/build.gradle.kts index a30d90427..7816ec3d0 100644 --- a/FloconAndroid/network/okhttp-interceptor/build.gradle.kts +++ b/FloconAndroid/network/okhttp-interceptor/build.gradle.kts @@ -1,98 +1,32 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - id("com.vanniktech.maven.publish") version "0.34.0" -} - -android { - namespace = "io.github.openflocon.flocon.okhttp" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -dependencies { - - api(project(":network:core")) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.jetbrains.kotlinx.coroutines.core) - implementation(libs.jetbrains.kotlinx.coroutines.android) - - implementation(platform(libs.okhttp.bom)) - implementation(libs.okhttp3.okhttp) - implementation(libs.brotli.dec) - - testImplementation(libs.junit) -} - - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-okhttp-interceptor", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) - - pom { - name = "Flocon OkHttp Interceptor" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } -} \ No newline at end of file +plugins { + id("flocon.android.library") + id("flocon.publish") +} + +android { + namespace = "io.github.openflocon.flocon.okhttp" +} + +dependencies { + + api(projects.network.core) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) + implementation(libs.brotli.dec) + + testImplementation(libs.junit) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-okhttp-interceptor", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index e16ed5784..9d077af4c 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -8,7 +8,7 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.apollo) - id("com.google.protobuf") + alias(libs.plugins.protobuf) } android { @@ -82,27 +82,27 @@ dependencies { //implementation("io.github.openflocon:flocon-okhttp-interceptor-no-op:$floconVersion") implementation("io.github.openflocon:flocon-ktor-interceptor:$floconVersion") } else { - debugImplementation(project(":flocon")) - releaseImplementation(project(":flocon-no-op")) + debugImplementation(projects.flocon) + releaseImplementation(projects.floconNoOp) - debugImplementation(project(":deeplinks")) - releaseImplementation(project(":deeplinks-no-op")) + debugImplementation(projects.deeplinks) + releaseImplementation(projects.deeplinksNoOp) debugImplementation(project(":database:room")) releaseImplementation(project(":database:room-no-op")) debugImplementation(project(":database:room3")) releaseImplementation(project(":database:room3-no-op")) - debugImplementation(project(":network:okhttp-interceptor")) - releaseImplementation(project(":network:okhttp-interceptor-no-op")) + debugImplementation(projects.network.okhttpInterceptor) + releaseImplementation(projects.network.okhttpInterceptorNoOp) - implementation(project(":grpc:grpc-interceptor-lite")) + implementation(projects.grpc.grpcInterceptorLite) - debugImplementation(project(":network:ktor-interceptor")) - releaseImplementation(project(":network:ktor-interceptor-no-op")) + debugImplementation(projects.network.ktorInterceptor) + releaseImplementation(projects.network.ktorInterceptorNoOp) - debugImplementation(project(":datastores")) - releaseImplementation(project(":datastores-no-op")) + debugImplementation(projects.datastores) + releaseImplementation(projects.datastoresNoOp) } @@ -168,12 +168,12 @@ apollo { protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.25.1" + artifact = libs.protobuf.protoc.get().toString() } generateProtoTasks { - val protocGenJava = "io.grpc:protoc-gen-grpc-java:1.73.0" - val protocGenKotlin = "io.grpc:protoc-gen-grpc-kotlin:1.4.3" + ":jdk8@jar" + val protocGenJava = libs.grpc.gen.java.get().toString() + val protocGenKotlin = libs.grpc.gen.kotlin.get().toString() + ":jdk8@jar" plugins { id("java") { diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index 64301aaa6..605e80924 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -33,8 +33,8 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) - implementation(project(":network:ktor-interceptor")) + implementation(projects.flocon) + implementation(projects.network.ktorInterceptor) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index 748d46048..2521b305b 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -1,4 +1,6 @@ -rootProject.name = "Flocon Sample App" +rootProject.name = "Flocon-Sample-App" + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { @@ -7,6 +9,7 @@ pluginManagement { gradlePluginPortal() } } + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -39,3 +42,6 @@ include(":database:room") include(":database:room-no-op") include(":database:room3") include(":database:room3-no-op") + +includeBuild("build-logic") + From dc836f788889ed4c0512046a5b8889a4a3ff481f Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Thu, 7 May 2026 15:43:45 +0200 Subject: [PATCH 22/38] fix: Build --- .../sample-multiplatform/build.gradle.kts | 1 + .../flocon/myapplication/multi/Databases.kt | 16 +++--- .../myapplication/multi/MainActivity.kt | 8 +-- .../flocon/myapplication/multi/ui/App.kt | 51 ++++++++----------- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index 605e80924..89d400280 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -34,6 +34,7 @@ kotlin { val commonMain by getting { dependencies { implementation(projects.flocon) + implementation(projects.deeplinks) implementation(projects.network.ktorInterceptor) implementation(libs.kotlinx.coroutines.core) diff --git a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/Databases.kt b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/Databases.kt index 6a6199926..2fb927254 100644 --- a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/Databases.kt +++ b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/Databases.kt @@ -2,11 +2,8 @@ package io.github.openflocon.flocon.myapplication.multi import android.content.Context import androidx.room.Room -import androidx.room.RoomDatabase import io.github.openflocon.flocon.myapplication.multi.database.DogDatabase import io.github.openflocon.flocon.myapplication.multi.database.FoodDatabase -import io.github.openflocon.flocon.plugins.database.floconLogDatabaseQuery -import java.util.concurrent.Executor import java.util.concurrent.Executors object Databases { @@ -14,17 +11,20 @@ object Databases { private var dogDatabase: DogDatabase? = null fun getDogDatabase(context: Context): DogDatabase { - val dbName = "dogs_database" + "dogs_database" return dogDatabase ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, DogDatabase::class.java, "dogs_database" ) - .setQueryCallback({ sqlQuery, bindArgs -> floconLogDatabaseQuery( - dbName = dbName, sqlQuery = sqlQuery, bindArgs = bindArgs - ) }, Executors.newSingleThreadExecutor()) - .fallbackToDestructiveMigration().build() +// .setQueryCallback({ sqlQuery, bindArgs -> +// floconLogDatabaseQuery( +// dbName = dbName, sqlQuery = sqlQuery, bindArgs = bindArgs +// ) +// }, Executors.newSingleThreadExecutor()) + .fallbackToDestructiveMigration(dropAllTables = true) + .build() dogDatabase = instance instance } diff --git a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt index 3ad3d762a..89253a000 100644 --- a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt +++ b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt @@ -5,7 +5,7 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.ktor.FloconKtorPlugin import io.github.openflocon.flocon.myapplication.multi.Databases.getDogDatabase @@ -14,6 +14,7 @@ import io.github.openflocon.flocon.myapplication.multi.database.initializeDataba import io.github.openflocon.flocon.myapplication.multi.sharedpreferences.initializeSharedPreferences import io.github.openflocon.flocon.myapplication.multi.ui.App import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks +import io.github.openflocon.flocon.startFlocon import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp @@ -52,10 +53,9 @@ class MainActivity : ComponentActivity() { ) FloconLogger.enabled = true - Flocon.initialize(this) { - install(FloconDeeplinks) { - } + startFlocon(FloconContext(this)) { + install(FloconDeeplinks) } setContent { diff --git a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/App.kt b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/App.kt index 140e68bf3..02afbfb09 100644 --- a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/App.kt +++ b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/App.kt @@ -2,7 +2,6 @@ package io.github.openflocon.flocon.myapplication.multi.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -16,14 +15,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.github.openflocon.flocon.myapplication.multi.DummyHttpKtorCaller import io.github.openflocon.flocon.myapplication.multi.dashboard.initializeDashboard -import io.github.openflocon.flocon.myapplication.multi.database.model.DogEntity -import io.github.openflocon.flocon.plugins.analytics.floconAnalytics -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent -import io.github.openflocon.flocon.plugins.analytics.model.analyticsProperty -import io.github.openflocon.flocon.plugins.tables.floconTable -import io.github.openflocon.flocon.plugins.tables.model.toParam -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlin.random.Random @Composable @@ -44,7 +35,7 @@ fun App() { text = "Flocon Multi App", style = MaterialTheme.typography.headlineMedium ) - + Column( modifier = Modifier.fillMaxWidth(), ) { @@ -64,32 +55,32 @@ fun App() { } Button( onClick = { - val value = Random.nextInt(from = 0, until = 1000).toString() - floconTable("analytics").log( - "name" toParam "new name $value", - "value1" toParam "value1 $value", - "value2" toParam "value2 $value", - ) + Random.nextInt(from = 0, until = 1000).toString() +// floconTable("analytics").log( +// "name" toParam "new name $value", +// "value1" toParam "value1 $value", +// "value2" toParam "value2 $value", +// ) } ) { Text("send table event") } Button( onClick = { - floconAnalytics("firebase").logEvents( - AnalyticsEvent( - eventName = "clicked user", - "userId" analyticsProperty "1024", - "username" analyticsProperty "florent", - "index" analyticsProperty "3", - ), - AnalyticsEvent( - eventName = "opened profile", - "userId" analyticsProperty "2048", - "username" analyticsProperty "kevin", - "age" analyticsProperty "34", - ), - ) +// floconAnalytics("firebase").logEvents( +// AnalyticsEvent( +// eventName = "clicked user", +// "userId" analyticsProperty "1024", +// "username" analyticsProperty "florent", +// "index" analyticsProperty "3", +// ), +// AnalyticsEvent( +// eventName = "opened profile", +// "userId" analyticsProperty "2048", +// "username" analyticsProperty "kevin", +// "age" analyticsProperty "34", +// ), +// ) } ) { Text("send analytics event") From 38d160e0e1a91e24a8877b922dff1463060710b1 Mon Sep 17 00:00:00 2001 From: Raphael Teyssandier Date: Wed, 13 May 2026 14:09:40 +0200 Subject: [PATCH 23/38] 2.0.0 - Table (#512) --- .../flocon/database/core/FloconDatabase.kt | 7 +- .../database/core/FloconDatabasePluginImpl.kt | 35 ++-- .../fromdevice/DatabaseExecuteSqlResponse.kt | 25 --- .../fromdevice/sql/DatabaseQueryLogModel.kt | 15 +- .../fromdevice/sql/DeviceDataBaseDataModel.kt | 6 - .../sql/QueryResultReceivedDataModel.kt | 9 +- .../model/todevice/DatabaseQueryMessage.kt | 15 +- FloconAndroid/database/room/build.gradle.kts | 7 - .../database/room3-no-op/build.gradle.kts | 2 + FloconAndroid/database/room3/build.gradle.kts | 4 - .../datastores-no-op/build.gradle.kts | 58 +++---- FloconAndroid/datastores/build.gradle.kts | 58 +++---- .../plugins/deeplinks/FloconDeeplinks.kt | 13 +- .../flocon/plugins/deeplinks/Mapping.kt | 16 +- FloconAndroid/flocon/build.gradle.kts | 163 +++++++++--------- .../FloconCrashReporterDataSource.android.kt | 13 +- .../io/github/openflocon/flocon/Flocon.kt | 23 ++- .../openflocon/flocon/FloconConfiguration.kt | 44 +++-- .../openflocon/flocon/FloconEncoding.kt | 5 +- .../github/openflocon/flocon/FloconPlugin.kt | 8 +- .../flocon/client/FloconClientImpl.kt | 78 +++++---- .../openflocon/flocon/core/FloconEncoder.kt | 36 +++- .../flocon/core/FloconFileSender.kt | 2 +- .../flocon/core/FloconMessageSender.kt | 4 +- .../flocon/error/PluginNotInitialized.kt | 1 + .../flocon/model/FloconMessageFromServer.kt | 13 -- .../flocon/model/FloconMessageToServer.kt | 6 - .../analytics/FloconAnalyticsPlugin.kt | 20 ++- .../analytics/mapper/AnalyticsItemsMapper.kt | 12 +- .../FloconCrashReporterPlugin.kt | 7 +- .../crashreporter/model/CrashReportMapper.kt | 22 --- .../dashboard/FloconDashboardPlugin.kt | 19 +- .../ToDeviceCheckBoxValueChangedMessage.kt | 17 +- .../todevice/ToDeviceSubmittedFormMessage.kt | 15 +- .../ToDeviceSubmittedTextFieldMessage.kt | 15 +- .../fromdevice/DatabaseExecuteSqlResponse.kt | 60 ------- .../model/fromdevice/DatabaseQueryLogModel.kt | 14 +- .../fromdevice/DeviceDataBaseDataModel.kt | 8 +- .../QueryResultReceivedDataModel.kt | 8 +- .../model/todevice/DatabaseQueryMessage.kt | 15 +- .../plugins/device/FloconDevicePluginImpl.kt | 4 +- .../fromdevice/RegisterDeviceDataModel.kt | 10 +- .../flocon/plugins/files/FloconFilesPlugin.kt | 25 +-- .../model/fromdevice/FilesResultDataModel.kt | 8 +- .../todevice/ToDeviceDeleteFileMessage.kt | 15 +- .../todevice/ToDeviceDeleteFilesMessage.kt | 15 +- .../ToDeviceDeleteFolderContentMessage.kt | 15 +- .../model/todevice/ToDeviceGetFileMessage.kt | 15 +- .../model/todevice/ToDeviceGetFilesMessage.kt | 16 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 6 +- .../model/FloconPreferenceWrapper.kt | 10 +- .../SharedPreferenceValueResultDataModel.kt | 8 +- ...oDeviceEditSharedPreferenceValueMessage.kt | 15 +- ...ToDeviceGetSharedPreferenceValueMessage.kt | 15 +- .../todevice/ToDeviceGetSharedPrefsMessage.kt | 15 +- .../analytics/FloconAnalyticsPlugin.kt | 43 ----- .../analytics/builder/AnalyticsBuilder.kt | 32 ---- .../analytics/model/AnalyticsEvent.kt | 11 -- .../model/AnalyticsPropertiesConfig.kt | 11 -- .../pluginsold/analytics/model/TableItem.kt | 9 - .../pluginsold/device/FloconDevicePlugin.kt | 10 +- .../pluginsold/files/FloconFilesPlugin.kt | 5 +- .../sharedprefs/FloconSharedPrefsPlugin.kt | 5 +- .../pluginsold/tables/FloconTablesPlugin.kt | 51 ------ .../pluginsold/tables/builder/TableBuilder.kt | 20 --- .../tables/model/TableColumnConfig.kt | 11 -- .../pluginsold/tables/model/TableItem.kt | 8 - .../database/FloconDatabasePlugin.ios.kt | 1 - .../database/FloconDatabasePlugin.jvm.kt | 1 - .../gradle/gradle-daemon-jvm.properties | 13 ++ FloconAndroid/gradle/libs.versions.toml | 4 +- .../grpc-interceptor-base/build.gradle.kts | 58 +++---- .../FloconNetworkDataSource.android.kt | 9 +- .../FloconNetworkDataSourceAndroid.kt | 25 +-- .../flocon/network/core/FloconNetwork.kt | 5 +- .../datasource/FloconNetworkDataSource.kt | 4 +- .../network/core/mapper/BadQualityToJson.kt | 21 --- .../core/mapper/FloconNetworkRequestToJson.kt | 100 +++++------ .../network/core/mapper/MockResponseToJson.kt | 24 --- .../flocon/network/core/mapper/Websocket.kt | 18 +- .../core/plugin/FloconNetworkPluginImpl.kt | 32 ++-- .../okhttp-interceptor-no-op/build.gradle.kts | 47 +++-- .../okhttp-interceptor/build.gradle.kts | 62 ++++--- .../sample-android-only/build.gradle.kts | 3 + .../flocon/myapplication/MainActivity.kt | 12 +- .../table/InitializeDashboard.kt | 11 -- .../myapplication/table/InitializeTable.kt | 13 ++ FloconAndroid/settings.gradle.kts | 5 + FloconAndroid/tables-no-op/build.gradle.kts | 117 +++++++++++++ .../flocon/plugins/tables/FloconTablesNoOp.kt | 43 +++++ .../flocon/plugins/tables/model/TableItem.kt | 4 + FloconAndroid/tables/build.gradle.kts | 119 +++++++++++++ .../plugins/tables/FloconTablesPlugin.kt | 35 +++- .../flocon/plugins/tables/dsl/TableItemDsl.kt | 35 ++++ .../plugins/tables/model/TableColumnConfig.kt | 6 + .../flocon/plugins/tables/model/TableItem.kt | 20 +-- 96 files changed, 976 insertions(+), 1177 deletions(-) delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportMapper.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseExecuteSqlResponse.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/FloconTablesPlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableColumnConfig.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableItem.kt create mode 100644 FloconAndroid/gradle/gradle-daemon-jvm.properties delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeDashboard.kt create mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt create mode 100644 FloconAndroid/tables-no-op/build.gradle.kts create mode 100644 FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesNoOp.kt create mode 100644 FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt create mode 100644 FloconAndroid/tables/build.gradle.kts rename FloconAndroid/{flocon => tables}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt (62%) create mode 100644 FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt create mode 100644 FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt rename FloconAndroid/{flocon => tables}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt (63%) diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt index eddb854db..7e5e49832 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/FloconDatabase.kt @@ -5,6 +5,7 @@ import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconEncoding import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.dsl.FloconMarker @@ -21,12 +22,14 @@ object FloconDatabase : FloconPluginFactory + override val providers: List, + private val encoder: FloconEncoder ) : FloconPlugin, FloconDatabasePlugin { override val key: String = Protocol.FromDevice.Database.Plugin @@ -42,7 +43,7 @@ internal class FloconDatabasePluginImpl( Protocol.ToDevice.Database.Method.GetDatabases -> sendAllDatabases(sender) Protocol.ToDevice.Database.Method.Query -> { - val queryMessage = DatabaseQueryMessage.fromJson(message = body) ?: return + val queryMessage = encoder.decode(body) ?: return val databaseModel = registeredDatabases.value .find { it.id == queryMessage.database } @@ -56,11 +57,12 @@ internal class FloconDatabasePluginImpl( sender.send( plugin = Protocol.FromDevice.Database.Plugin, method = Protocol.FromDevice.Database.Method.Query, - body = QueryResultDataModel( - requestId = queryMessage.requestId, - result = FloconEncoder.json.encodeToString(result) + body = encoder.encode( + QueryResultDataModel( + requestId = queryMessage.requestId, + result = encoder.encode(result) + ) ) - .toJson() ) } catch (t: Throwable) { FloconLogger.logError("Database parsing error", t) @@ -74,7 +76,7 @@ internal class FloconDatabasePluginImpl( } @OptIn(FloconMarker::class) - private suspend fun sendAllDatabases(sender: FloconMessageSender) { + private fun sendAllDatabases(sender: FloconMessageSender) { val databases = providers.flatMap { it.getAllDataBases(emptyList()) } val all = registeredDatabases.updateAndGet { it + databases } .map { DeviceDataBaseDataModel(id = it.id, name = it.displayName) } @@ -83,7 +85,7 @@ internal class FloconDatabasePluginImpl( sender.send( plugin = Protocol.FromDevice.Database.Plugin, method = Protocol.FromDevice.Database.Method.GetDatabases, - body = listDeviceDataBaseDataModelToJson(all), + body = encoder.encode(all), ) } catch (t: Throwable) { FloconLogger.logError("Database parsing error", t) @@ -100,13 +102,14 @@ internal class FloconDatabasePluginImpl( sender.send( plugin = Protocol.FromDevice.Database.Plugin, method = Protocol.FromDevice.Database.Method.LogQuery, - body = DatabaseQueryLogModel( - dbName = dbName, - sqlQuery = sqlQuery, - bindArgs = bindArgs.map { it.toString() }, - timestamp = currentTimeMillis() + body = encoder.encode( + DatabaseQueryLogModel( + dbName = dbName, + sqlQuery = sqlQuery, + bindArgs = bindArgs.map { it.toString() }, + timestamp = currentTimeMillis() + ) ) - .toJson() ) } catch (t: Throwable) { FloconLogger.logError("Database logging error", t) diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt index 9539a01a0..2987b5aed 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/DatabaseExecuteSqlResponse.kt @@ -1,11 +1,7 @@ package io.github.openflocon.flocon.database.core.model.fromdevice -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.put @Serializable sealed interface DatabaseExecuteSqlResponse : DatabaseExecuteResponse { @@ -45,24 +41,3 @@ sealed interface DatabaseExecuteSqlResponse : DatabaseExecuteResponse { val originalSql: String = "", // SQL query that caused the error (optional) ) : DatabaseExecuteSqlResponse } - -fun DatabaseExecuteSqlResponse.toJson(): String { - val jsonEncoder = FloconEncoder.json - val thisAsJson = jsonEncoder.encodeToJsonElement(this) - - val type = when (this) { - is DatabaseExecuteSqlResponse.Error -> "Error" - is DatabaseExecuteSqlResponse.Insert -> "Insert" - DatabaseExecuteSqlResponse.RawSuccess -> "RawSuccess" - is DatabaseExecuteSqlResponse.Select -> "Select" - is DatabaseExecuteSqlResponse.UpdateDelete -> "UpdateDelete" - } - - return buildJsonObject { - put("type", type) - put( - "body", - thisAsJson.toString() - ) // warning : the desktop is waiting for a string representation of the json here - }.toString() -} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DatabaseQueryLogModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DatabaseQueryLogModel.kt index dbb7a4507..456b10fe4 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DatabaseQueryLogModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DatabaseQueryLogModel.kt @@ -1,9 +1,6 @@ package io.github.openflocon.flocon.database.core.model.fromdevice.sql -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString @Serializable data class DatabaseQueryLogModel( @@ -11,14 +8,4 @@ data class DatabaseQueryLogModel( val sqlQuery: String, val bindArgs: List?, val timestamp: Long, -) { - fun toJson(): String { - return FloconEncoder.json.encodeToString(this) - } - - companion object { - fun fromJson(json: String): DatabaseQueryLogModel { - return FloconEncoder.json.decodeFromString(json) - } - } -} +) diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt index 182dcc0e2..74891fb6c 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/DeviceDataBaseDataModel.kt @@ -1,15 +1,9 @@ package io.github.openflocon.flocon.database.core.model.fromdevice.sql -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable data class DeviceDataBaseDataModel( val id: String, val name: String ) - -fun listDeviceDataBaseDataModelToJson(items: List) : String { - return FloconEncoder.json.encodeToString(items) -} diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt index a05cfdae1..e916b787c 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/fromdevice/sql/QueryResultReceivedDataModel.kt @@ -1,16 +1,9 @@ package io.github.openflocon.flocon.database.core.model.fromdevice.sql -import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteResponse import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable internal data class QueryResultDataModel( val requestId: String, val result: String -) { - fun toJson(): String { - return FloconEncoder.json.encodeToString(this) - } -} +) \ No newline at end of file diff --git a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/todevice/DatabaseQueryMessage.kt b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/todevice/DatabaseQueryMessage.kt index cd81313c8..c0451d778 100644 --- a/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/todevice/DatabaseQueryMessage.kt +++ b/FloconAndroid/database/core/src/commonMain/kotlin/io/github/openflocon/flocon/database/core/model/todevice/DatabaseQueryMessage.kt @@ -1,7 +1,5 @@ package io.github.openflocon.flocon.database.core.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable @@ -9,15 +7,4 @@ data class DatabaseQueryMessage( val query: String, val requestId: String, val database: String, -) { - companion object { - fun fromJson(message: String): DatabaseQueryMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} +) diff --git a/FloconAndroid/database/room/build.gradle.kts b/FloconAndroid/database/room/build.gradle.kts index eb82a4a12..86da03840 100644 --- a/FloconAndroid/database/room/build.gradle.kts +++ b/FloconAndroid/database/room/build.gradle.kts @@ -20,11 +20,6 @@ kotlin { implementation(libs.kotlinx.coroutines.android) } } - - val jvmMain by getting { - dependencies { - } - } val iosX64Main by getting val iosArm64Main by getting @@ -42,8 +37,6 @@ android { namespace = "io.github.openflocon.flocon.database.room" } - - mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, diff --git a/FloconAndroid/database/room3-no-op/build.gradle.kts b/FloconAndroid/database/room3-no-op/build.gradle.kts index 470e7406c..60cdfa68a 100644 --- a/FloconAndroid/database/room3-no-op/build.gradle.kts +++ b/FloconAndroid/database/room3-no-op/build.gradle.kts @@ -22,6 +22,8 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + implementation(project(":network:core-no-op")) + implementation(libs.ktor.client.core) implementation(project(":database:core-no-op")) } } diff --git a/FloconAndroid/database/room3/build.gradle.kts b/FloconAndroid/database/room3/build.gradle.kts index cf12f31f2..3e7385b5f 100644 --- a/FloconAndroid/database/room3/build.gradle.kts +++ b/FloconAndroid/database/room3/build.gradle.kts @@ -32,13 +32,11 @@ kotlin { val androidMain by getting { dependencies { - implementation(libs.brotli.dec) } } val jvmMain by getting { dependencies { - implementation(libs.brotli.dec) } } @@ -72,14 +70,12 @@ android { defaultConfig { minSdk = 23 } - compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } - mavenPublishing { publishToMavenCentral(automaticRelease = true) diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index d89542b0c..5f4154d85 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -1,30 +1,30 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id("flocon.android.library") - id("flocon.publish") -} - -android { - namespace = "io.github.openflocon.flocon.datastores" -} - - -dependencies { - - implementation(projects.flocon) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.kotlinx.coroutines.core) - - implementation(libs.androidx.datastore.preferences) -} - - -mavenPublishing { - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-datastores-no-op", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.android.library") + id("flocon.publish") +} + +android { + namespace = "io.github.openflocon.flocon.datastores" +} + + +dependencies { + + implementation(projects.flocon) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.androidx.datastore.preferences) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-datastores-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) } \ No newline at end of file diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 6974dfad8..19c695d5f 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -1,30 +1,30 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id("flocon.android.library") - id("flocon.publish") -} - -android { - namespace = "io.github.openflocon.flocon.datastores" -} - -dependencies { - - implementation(projects.flocon) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.android) - - implementation(libs.androidx.datastore.preferences) -} - - -mavenPublishing { - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-datastores", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.android.library") + id("flocon.publish") +} + +android { + namespace = "io.github.openflocon.flocon.datastores" +} + +dependencies { + + implementation(projects.flocon) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + implementation(libs.androidx.datastore.preferences) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-datastores", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) } \ No newline at end of file diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt index 83c8ef103..856d79497 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt @@ -7,7 +7,9 @@ import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.encode import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel @@ -23,12 +25,14 @@ object FloconDeeplinks : FloconPluginFactory, private val variables: List, private val sender: FloconMessageSender, + private val encoder: FloconEncoder ) : FloconPlugin, FloconDeeplinksPlugin { override val key: String = "DEEP_LINK" @@ -56,7 +61,7 @@ internal class FloconDeeplinksPluginImpl( ) } - suspend fun registerDeeplinks( + fun registerDeeplinks( deeplinks: List, variables: List ) { @@ -64,7 +69,7 @@ internal class FloconDeeplinksPluginImpl( sender.send( plugin = Protocol.FromDevice.Deeplink.Plugin, method = Protocol.FromDevice.Deeplink.Method.GetDeeplinks, - body = createJson(deeplinks = deeplinks, variables = variables) + body = encoder.encode(createRemote(deeplinks = deeplinks, variables = variables)) ) } catch (t: Throwable) { t.printStackTrace() diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt index 89caedf6c..8e0194e42 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt @@ -1,24 +1,18 @@ package io.github.openflocon.flocon.plugins.deeplinks -import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkVariableRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinksRemote -import kotlinx.serialization.encodeToString -internal fun createJson( +internal fun createRemote( deeplinks: List, variables: List -): String { - val dto = DeeplinksRemote( - deeplinks = deeplinks.map(DeeplinkModel::toRemote), - variables = variables.map(DeeplinkVariable::toRemote) - ) - - return FloconEncoder.json.encodeToString(dto) -} +) = DeeplinksRemote( + deeplinks = deeplinks.map(DeeplinkModel::toRemote), + variables = variables.map(DeeplinkVariable::toRemote) +) internal fun DeeplinkModel.toRemote(): DeeplinkRemote = DeeplinkRemote( label = label, diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index ab063ba78..a60a1be31 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -1,82 +1,81 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id("flocon.kotlin.multiplatform") - alias(libs.plugins.kotlin.serialization) - id("flocon.publish") - alias(libs.plugins.buildconfig) -} - - -kotlin { - sourceSets { - val commonMain by getting { - dependencies { - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.json) - } - } - - val androidMain by getting { - dependencies { - implementation(libs.kotlinx.coroutines.android) - implementation(libs.jakewharton.process.phoenix) - implementation("com.squareup.okhttp3:okhttp:4.12.0") - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.cio) - - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.client.logging) - implementation(libs.ktor.serialization.kotlinx.json) - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.darwin) - - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.client.logging) - implementation(libs.ktor.serialization.kotlinx.json) - - // to store the device id - implementation("com.russhwolf:multiplatform-settings:1.3.0") - implementation(libs.androidx.sqlite.bundled) - } - } - } -} - -buildConfig { - packageName("io.github.openflocon.flocondesktop") - - buildConfigField("APP_VERSION", System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String) -} - -android { - namespace = "io.github.openflocon.flocon" - - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - sourceSets["main"].res.srcDirs("src/androidMain/res") -} - -mavenPublishing { - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) -} \ No newline at end of file +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.kotlin.multiplatform") + alias(libs.plugins.kotlin.serialization) + id("flocon.publish") + alias(libs.plugins.buildconfig) +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.kotlinx.coroutines.android) + implementation(libs.jakewharton.process.phoenix) + implementation("com.squareup.okhttp3:okhttp:4.12.0") + } + } + + val jvmMain by getting { + dependencies { + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.serialization.kotlinx.json) + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + dependencies { + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.darwin) + + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.serialization.kotlinx.json) + + // to store the device id + implementation("com.russhwolf:multiplatform-settings:1.3.0") + implementation(libs.androidx.sqlite.bundled) + } + } + } +} + +buildConfig { + packageName("io.github.openflocon.flocondesktop") + + buildConfigField("APP_VERSION", System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String) +} + +android { + namespace = "io.github.openflocon.flocon" + + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].res.srcDirs("src/androidMain/res") +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt index f7e9a720a..927c2a107 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt @@ -1,16 +1,17 @@ package io.github.openflocon.flocon.pluginsold.crashreporter import android.content.Context -import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.core.decode +import io.github.openflocon.flocon.core.encode import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporterDataSource import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel -import io.github.openflocon.flocon.plugins.crashreporter.model.crashReportFromJson -import io.github.openflocon.flocon.plugins.crashreporter.model.toJson import java.io.File internal class FloconCrashReporterDataSourceAndroid( - private val context: Context + private val context: Context, + private val encoder: FloconEncoder ) : FloconCrashReporterDataSource { private val crashesDir = File(context.filesDir, "flocon_crashes") @@ -22,7 +23,7 @@ internal class FloconCrashReporterDataSourceAndroid( override fun saveCrash(crash: CrashReportDataModel) { try { val file = File(crashesDir, "${crash.crashId}.json") - val jsonString = crash.toJson() + val jsonString = encoder.encode(crash) file.writeText(jsonString) } catch (t: Throwable) { FloconLogger.logError("Error saving crash", t) @@ -34,7 +35,7 @@ internal class FloconCrashReporterDataSourceAndroid( crashesDir.listFiles() ?.mapNotNull { file -> try { - crashReportFromJson(file.readText()) + encoder.decode(file.readText()) } catch (t: Throwable) { t.printStackTrace() null diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt index 6386e43ef..7a7c434df 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Flocon.kt @@ -3,18 +3,20 @@ package io.github.openflocon.flocon import io.github.openflocon.flocon.FloconApp.Client +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.decode import io.github.openflocon.flocon.dsl.FloconMarker -import io.github.openflocon.flocon.model.floconMessageFromServerFromJson +import io.github.openflocon.flocon.model.FloconMessageFromServer import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class Flocon internal constructor( private val config: FloconConfig, - private val plugins: List + private val plugins: List, + private val encoder: FloconEncoder ) { init { @@ -68,13 +70,18 @@ class Flocon internal constructor( private fun onMessageReceived(message: String) { println("Message received : $message") - config.scope.launch(Dispatchers.IO) { - floconMessageFromServerFromJson(message)?.let { messageFromServer -> - plugins.find { it.key == messageFromServer.plugin } + config.scope.launch { + try { + val serialized = encoder.decode(message) + ?: return@launch + + plugins.find { it.key == serialized.plugin } ?.onMessageReceived( - method = messageFromServer.method, - body = messageFromServer.body, + method = serialized.method, + body = serialized.body ) + } catch (throwable: Throwable) { + throwable.printStackTrace() } } } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt index 0d74f842c..68c39be0e 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconConfiguration.kt @@ -1,16 +1,21 @@ package io.github.openflocon.flocon import io.github.openflocon.flocon.client.FloconClient +import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.SupervisorJob +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.plus class FloconConfiguration internal constructor( private val config: FloconConfig ) { - private val plugins: MutableMap FloconPlugin> = mutableMapOf() + private val plugins: MutableMap FloconPlugin> = mutableMapOf() + + private var serializerModule = SerializersModule {} /** * Install a plugin with the given [factory] and optional [configure] block. @@ -19,21 +24,26 @@ class FloconConfiguration internal constructor( factory: FloconPluginFactory, configure: Config.() -> Unit = {} ) { - plugins[factory.pluginId] = { scope -> + plugins[factory.pluginId] = { scope, encoder -> val config = factory.createConfig(config.context) .apply { configure() } factory.install( pluginConfig = config, - floconConfig = scope + floconConfig = scope, + encoder = encoder ) } + + serializerModule += factory.createEncoding().serializersModule } - fun build(): List { - return plugins.values.map { it.invoke(config) } + fun build(encoder: FloconEncoder): List { + return plugins.values.map { it.invoke(config, encoder) } } + fun encoding() = serializerModule + } @ConsistentCopyVisibility @@ -43,17 +53,31 @@ data class FloconConfig internal constructor( val client: FloconClient ) -fun startFlocon(context: FloconContext, block: FloconConfiguration.() -> Unit) { +fun startFlocon( + context: FloconContext, + block: FloconConfiguration.() -> Unit +) { + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val client = FloconClient( + context = context, + scope = scope + ) val config = FloconConfig( context = context, - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), - client = FloconClient(context = context) + scope = scope, + client = client + ) + val configuration = FloconConfiguration( + config = config, ) - val configuration = FloconConfiguration(config = config) .apply(block) + val encoder = FloconEncoder(module = configuration.encoding()) + + client.setupEncoder(encoder) Flocon( config = config, - plugins = configuration.build() + plugins = configuration.build(encoder), + encoder = encoder ) } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconEncoding.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconEncoding.kt index a5f3c0dfe..a830472c6 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconEncoding.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconEncoding.kt @@ -1,18 +1,15 @@ package io.github.openflocon.flocon -import io.github.openflocon.flocon.dsl.FloconMarker import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule -@FloconMarker interface FloconEncoding { val serializersModule: SerializersModule } -@FloconMarker -internal class DefaultEncoding() : FloconEncoding { +internal class DefaultEncoding : FloconEncoding { override val serializersModule: SerializersModule get() = EmptySerializersModule() } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt index 948f13d2b..b8f76d15e 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/FloconPlugin.kt @@ -1,5 +1,6 @@ package io.github.openflocon.flocon +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.dsl.FloconMarker /** @@ -34,7 +35,6 @@ interface FloconPluginKey { interface FloconPluginFactory : FloconPluginKey { - @FloconMarker fun createEncoding(): FloconEncoding = DefaultEncoding() /** @@ -46,6 +46,10 @@ interface FloconPluginFactory Unit, @@ -47,47 +59,53 @@ class FloconClient internal constructor( webSocketClient.disconnect() } - override suspend fun send( + override fun send( plugin: String, method: String, body: String, ) { - webSocketClient.sendMessage( - message = FloconMessageToServer( - deviceId = appInfos.deviceId, - plugin = plugin, - body = body, - appName = appInfos.appName, - appPackageName = appInfos.appPackageName, - method = method, - deviceName = appInfos.deviceName, - appInstance = appInstance, - platform = appInfos.platform, - versionName = versionName, + scope.launch { + webSocketClient.sendMessage( + encoder.encode( + FloconMessageToServer( + deviceId = appInfos.deviceId, + plugin = plugin, + body = body, + appName = appInfos.appName, + appPackageName = appInfos.appPackageName, + method = method, + deviceName = appInfos.deviceName, + appInstance = appInstance, + platform = appInfos.platform, + versionName = versionName, + ) + ) ) - .toFloconMessageToServer(), - ) + } } @FloconMarker - override suspend fun send( + override fun send( file: FloconFile, infos: FloconFileInfo, ) { - httpClient.send( - address = address, - port = FLOCON_HTTP_PORT, - file = file, - infos = infos, - - deviceId = appInfos.deviceId, - appPackageName = appInfos.appPackageName, - appInstance = appInstance, - ) + scope.launch { + httpClient.send( + address = address, + port = FLOCON_HTTP_PORT, + file = file, + infos = infos, + deviceId = appInfos.deviceId, + appPackageName = appInfos.appPackageName, + appInstance = appInstance, + ) + } } - override suspend fun sendPendingMessages() { - webSocketClient.sendPendingMessages() + override fun sendPendingMessages() { + scope.launch { + webSocketClient.sendPendingMessages() + } } companion object { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt index cf5659731..51e738415 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt @@ -1,16 +1,42 @@ package io.github.openflocon.flocon.core +import kotlinx.serialization.KSerializer import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.serializer -object FloconEncoder { - val json = Json { +class FloconEncoder internal constructor( + val module: SerializersModule +) { + private val json = Json { ignoreUnknownKeys = true isLenient = true encodeDefaults = false + serializersModule = module + } - serializersModule = SerializersModule { + fun encode(serializer: KSerializer, body: T): String = json.encodeToString( + serializer = serializer, + value = body + ) - } - } + fun decode(serializer: KSerializer, body: String): T = json.decodeFromString( + deserializer = serializer, + string = body + ) + +} + +inline fun FloconEncoder.encode(body: T) = encode( + serializer = module.serializer(), + body = body +) + +inline fun FloconEncoder.decode(body: String): T? = try { + decode( + serializer = module.serializer(), + body = body + ) +} catch (_: Throwable) { + null } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt index 4d835a5a7..43924d572 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt @@ -7,6 +7,6 @@ import io.github.openflocon.flocon.model.FloconFileInfo internal interface FloconFileSender { @FloconMarker - suspend fun send(file: FloconFile, infos: FloconFileInfo) + fun send(file: FloconFile, infos: FloconFileInfo) } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt index a61ba4772..e2daa82cb 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconMessageSender.kt @@ -2,12 +2,12 @@ package io.github.openflocon.flocon.core interface FloconMessageSender { - suspend fun send( + fun send( plugin: String, method: String, body: String, ) - suspend fun sendPendingMessages() + fun sendPendingMessages() } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/error/PluginNotInitialized.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/error/PluginNotInitialized.kt index 60da21935..99bbac42c 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/error/PluginNotInitialized.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/error/PluginNotInitialized.kt @@ -2,5 +2,6 @@ package io.github.openflocon.flocon.error import io.github.openflocon.flocon.dsl.FloconMarker +// Maybe remove it, and make plugins nullable to avoid app crashing @FloconMarker fun pluginNotInitialized(pluginName: String): Nothing = error("$pluginName is not initialized") \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconMessageFromServer.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconMessageFromServer.kt index 68c813b3c..393ae404d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconMessageFromServer.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconMessageFromServer.kt @@ -1,20 +1,7 @@ package io.github.openflocon.flocon.model -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -internal fun floconMessageFromServerFromJson( - message: String, -): FloconMessageFromServer? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } -} - @Serializable internal data class FloconMessageFromServer( val plugin: String, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconMessageToServer.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconMessageToServer.kt index 8f8b081eb..50fe8f0f4 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconMessageToServer.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/model/FloconMessageToServer.kt @@ -1,8 +1,6 @@ package io.github.openflocon.flocon.model -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable internal class FloconMessageToServer( @@ -17,7 +15,3 @@ internal class FloconMessageToServer( val platform: String, // android, ios, desktop val versionName: String, // ex: 1.3.0 ) - -internal fun FloconMessageToServer.toFloconMessageToServer(): String { - return FloconEncoder.json.encodeToString(this) -} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index d805cbfd0..0705dc523 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -7,7 +7,10 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.encode +import io.github.openflocon.flocon.plugins.analytics.mapper.toRemote import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem class FloconAnalyticsConfig : FloconPluginConfig @@ -22,16 +25,19 @@ object FloconAnalytics : FloconPluginFactory) { analyticsItems.takeIf { it.isNotEmpty() }?.forEach { toSend -> try { -// sender.send( -// plugin = Protocol.FromDevice.Analytics.Plugin, -// method = Protocol.FromDevice.Analytics.Method.AddItems, -// body = analyticsItemsToJson(toSend) -// ) + sender.send( + plugin = Protocol.FromDevice.Analytics.Plugin, + method = Protocol.FromDevice.Analytics.Method.AddItems, + body = encoder.encode(listOf(toSend.toRemote())) + ) } catch (t: Throwable) { FloconLogger.logError("error on sendAnalytics", t) } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt index f93b42598..4bab74e3c 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt @@ -1,17 +1,7 @@ package io.github.openflocon.flocon.plugins.analytics.mapper -import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString - -internal fun analyticsItemsToJson(item: AnalyticsItem): String { - return FloconEncoder.json.encodeToString( - listOf( - item.toSerializable() - ) - ) -} @Serializable internal class AnalyticsItemSerializable( @@ -28,7 +18,7 @@ internal class AnalyticsPropertySerializable( val value: String, ) -internal fun AnalyticsItem.toSerializable(): AnalyticsItemSerializable { +internal fun AnalyticsItem.toRemote(): AnalyticsItemSerializable { return AnalyticsItemSerializable( id = id, analyticsTableId = analyticsTableId, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt index 8a61800e1..8fb7032d3 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt @@ -7,6 +7,7 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel import io.github.openflocon.flocon.utils.currentTimeMillis @@ -26,8 +27,7 @@ interface FloconCrashReporterPlugin : FloconPlugin { fun setupCrashHandler() } -object FloconCrashReporter : - FloconPluginFactory { +object FloconCrashReporter : FloconPluginFactory { override val name: String = "CrashReporter" override val pluginId: String = Protocol.ToDevice.Analytics.Plugin // Crash reporter is usually write-only but we can set an ID @@ -36,7 +36,8 @@ object FloconCrashReporter : override fun install( pluginConfig: FloconCrashReporterConfig, - floconConfig: FloconConfig + floconConfig: FloconConfig, + encoder: FloconEncoder ): FloconCrashReporterPlugin { val client = floconConfig.client as FloconMessageSender return FloconCrashReporterPluginImpl( diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportMapper.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportMapper.kt deleted file mode 100644 index dec1f3fb1..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportMapper.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.github.openflocon.flocon.plugins.crashreporter.model - -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder -import kotlinx.serialization.encodeToString - -internal fun CrashReportDataModel.toJson(): String { - return FloconEncoder.json.encodeToString(this) -} - -internal fun crashReportFromJson(jsonString: String): CrashReportDataModel? { - return try { - FloconEncoder.json.decodeFromString(jsonString) - } catch (t: Throwable) { - FloconLogger.logError("Crash report parsing error", t) - null - } -} - -internal fun crashReportsListToJson(crashes: List): String { - return FloconEncoder.json.encodeToString(crashes) -} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt index ee9d2ff6f..e2b14a159 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/FloconDashboardPlugin.kt @@ -7,7 +7,9 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.decode import io.github.openflocon.flocon.plugins.dashboard.mapper.toJson import io.github.openflocon.flocon.plugins.dashboard.model.DashboardCallback import io.github.openflocon.flocon.plugins.dashboard.model.DashboardConfig @@ -29,18 +31,17 @@ object FloconDashboard : FloconPluginFactory { - ToDeviceSubmittedFormMessage.fromJson(body)?.let { + encoder.decode(body)?.let { callbackMap[it.id]?.let { it as? DashboardCallback.FormCallback }?.actions?.invoke( it.values ) @@ -71,7 +72,7 @@ internal class FloconDashboardPluginImpl( } Protocol.ToDevice.Dashboard.Method.OnTextFieldSubmitted -> { - ToDeviceSubmittedTextFieldMessage.fromJson(body)?.let { + encoder.decode(body)?.let { callbackMap[it.id]?.let { it as? DashboardCallback.TextFieldCallback }?.action?.invoke( it.value ) @@ -79,7 +80,7 @@ internal class FloconDashboardPluginImpl( } Protocol.ToDevice.Dashboard.Method.OnCheckBoxValueChanged -> { - ToDeviceCheckBoxValueChangedMessage.fromJson(body)?.let { + encoder.decode(body)?.let { callbackMap[it.id]?.let { it as? DashboardCallback.CheckBoxCallback }?.action?.invoke( it.value ) @@ -108,7 +109,7 @@ internal class FloconDashboardPluginImpl( }, ) - dashboards.put(dashboardConfig.id, dashboardConfig) + dashboards[dashboardConfig.id] = dashboardConfig try { sender.send( diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceCheckBoxValueChangedMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceCheckBoxValueChangedMessage.kt index 0af4c78d4..59dfeb855 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceCheckBoxValueChangedMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceCheckBoxValueChangedMessage.kt @@ -1,22 +1,9 @@ package io.github.openflocon.flocon.plugins.dashboard.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable internal data class ToDeviceCheckBoxValueChangedMessage( val id: String, - val value: Boolean, -) { - companion object { - fun fromJson(message: String): ToDeviceCheckBoxValueChangedMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} \ No newline at end of file + val value: Boolean +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceSubmittedFormMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceSubmittedFormMessage.kt index ef406d702..9660934b9 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceSubmittedFormMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceSubmittedFormMessage.kt @@ -1,22 +1,9 @@ package io.github.openflocon.flocon.plugins.dashboard.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable internal data class ToDeviceSubmittedFormMessage( val id: String, val values: Map -) { - companion object { - fun fromJson(message: String): ToDeviceSubmittedFormMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceSubmittedTextFieldMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceSubmittedTextFieldMessage.kt index c789795aa..cc70676e5 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceSubmittedTextFieldMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/dashboard/model/todevice/ToDeviceSubmittedTextFieldMessage.kt @@ -1,22 +1,9 @@ package io.github.openflocon.flocon.plugins.dashboard.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable internal data class ToDeviceSubmittedTextFieldMessage( val id: String, val value: String, -) { - companion object { - fun fromJson(message: String): ToDeviceSubmittedTextFieldMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseExecuteSqlResponse.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseExecuteSqlResponse.kt deleted file mode 100644 index 5ca940607..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseExecuteSqlResponse.kt +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.openflocon.flocon.plugins.database.model.fromdevice - -import io.github.openflocon.flocon.core.FloconEncoder -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.put - -@Serializable -internal sealed interface DatabaseExecuteSqlResponse { - - @Serializable - // Case for successful SELECT queries - class Select( - val columns: List, - val values: List> - ) : DatabaseExecuteSqlResponse - - // Case for successful INSERT queries - @Serializable - class Insert( - val insertedId: Long - ) : DatabaseExecuteSqlResponse - - // Case for successful UPDATE or DELETE queries - @Serializable - class UpdateDelete( - val affectedCount: Int - ) : DatabaseExecuteSqlResponse - - // Case for successful "raw" queries (CREATE TABLE, DROP TABLE, etc.) - @Serializable - object RawSuccess : DatabaseExecuteSqlResponse - - // Case for an SQL execution error - @Serializable - class Error( - val message: String, // Detailed error message - val originalSql: String, // SQL query that caused the error (optional) - ) : DatabaseExecuteSqlResponse -} - -internal fun DatabaseExecuteSqlResponse.toJson(): String { - val jsonEncoder = FloconEncoder.json - val thisAsJson = jsonEncoder.encodeToJsonElement(this) - - val type = when (this) { - is DatabaseExecuteSqlResponse.Error -> "Error" - is DatabaseExecuteSqlResponse.Insert -> "Insert" - DatabaseExecuteSqlResponse.RawSuccess -> "RawSuccess" - is DatabaseExecuteSqlResponse.Select -> "Select" - is DatabaseExecuteSqlResponse.UpdateDelete -> "UpdateDelete" - } - - return buildJsonObject { - put("type", type) - put("body", thisAsJson.toString()) // warning : the desktop is waiting for a string representation of the json here - }.toString() -} - diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseQueryLogModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseQueryLogModel.kt index e8644e329..d75c31fb5 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseQueryLogModel.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseQueryLogModel.kt @@ -1,8 +1,6 @@ package io.github.openflocon.flocon.plugins.database.model.fromdevice -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable internal data class DatabaseQueryLogModel( @@ -10,14 +8,4 @@ internal data class DatabaseQueryLogModel( val sqlQuery: String, val bindArgs: List?, val timestamp: Long, -) { - fun toJson(): String { - return FloconEncoder.json.encodeToString(this) - } - - companion object { - fun fromJson(json: String): DatabaseQueryLogModel { - return FloconEncoder.json.decodeFromString(json) - } - } -} +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DeviceDataBaseDataModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DeviceDataBaseDataModel.kt index 6842ced6f..6a24b7ea9 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DeviceDataBaseDataModel.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DeviceDataBaseDataModel.kt @@ -1,15 +1,9 @@ package io.github.openflocon.flocon.plugins.database.model.fromdevice -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable internal data class DeviceDataBaseDataModel( val id: String, val name: String, -) - -internal fun listDeviceDataBaseDataModelToJson(items: List) : String { - return FloconEncoder.json.encodeToString(items) -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/QueryResultReceivedDataModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/QueryResultReceivedDataModel.kt index 8ef4f899a..66f176021 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/QueryResultReceivedDataModel.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/QueryResultReceivedDataModel.kt @@ -1,15 +1,9 @@ package io.github.openflocon.flocon.plugins.database.model.fromdevice -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable internal data class QueryResultDataModel( val requestId: String, val result: String, -) { - fun toJson(): String { - return FloconEncoder.json.encodeToString(this) - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/todevice/DatabaseQueryMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/todevice/DatabaseQueryMessage.kt index c6b353d51..27508e328 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/todevice/DatabaseQueryMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/todevice/DatabaseQueryMessage.kt @@ -1,7 +1,5 @@ package io.github.openflocon.flocon.plugins.database.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable @@ -9,15 +7,4 @@ internal data class DatabaseQueryMessage( val query: String, val requestId: String, val database: String, -) { - companion object { - fun fromJson(message: String): DatabaseQueryMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt index 8bea31768..1d7ef5463 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt @@ -7,6 +7,7 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender class FloconDeviceConfig : FloconPluginConfig @@ -21,7 +22,8 @@ object FloconDevice : FloconPluginFactory(this) - } -} \ No newline at end of file + val serial: String +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt index 3dd6df539..9910d3dfd 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt @@ -8,8 +8,10 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconFileSender import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.decode import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.plugins.files.model.fromdevice.FileDataModel import io.github.openflocon.flocon.plugins.files.model.todevice.ToDeviceDeleteFileMessage @@ -32,13 +34,15 @@ object FloconFiles : FloconPluginFactory { override fun createConfig(context: FloconContext) = FloconFilesConfig() override fun install( pluginConfig: FloconFilesConfig, - floconConfig: FloconConfig + floconConfig: FloconConfig, + encoder: FloconEncoder ): FloconFilesPlugin { val client = floconConfig.client return FloconFilesPluginImpl( context = floconConfig.context, floconFileSender = client as FloconFileSender, - sender = client as FloconMessageSender + sender = client as FloconMessageSender, + encoder = encoder ) } } @@ -67,6 +71,7 @@ internal class FloconFilesPluginImpl( private val context: FloconContext, private val floconFileSender: FloconFileSender, private val sender: FloconMessageSender, + private val encoder: FloconEncoder ) : FloconPlugin, FloconFilesPlugin { override val key: String = "FILES" @@ -80,7 +85,7 @@ internal class FloconFilesPluginImpl( ) { when (method) { Protocol.ToDevice.Files.Method.ListFiles -> { - val listFilesMessage = ToDeviceGetFilesMessage.fromJson(message = body) ?: return + val listFilesMessage = encoder.decode(body) ?: return withFoldersSize.update { listFilesMessage.withFoldersSize } @@ -92,7 +97,7 @@ internal class FloconFilesPluginImpl( } Protocol.ToDevice.Files.Method.GetFile -> { - val getFileMessage = ToDeviceGetFileMessage.fromJson(message = body) ?: return + val getFileMessage = encoder.decode(body) ?: return fileDataSource.getFile(path = getFileMessage.path, isConstantPath = false) ?.let { file -> @@ -107,8 +112,7 @@ internal class FloconFilesPluginImpl( } Protocol.ToDevice.Files.Method.DeleteFile -> { - val deleteFilesMessage = - ToDeviceDeleteFileMessage.fromJson(message = body) ?: return + val deleteFilesMessage = encoder.decode(body) ?: return fileDataSource.deleteFile( path = deleteFilesMessage.filePath, @@ -122,8 +126,7 @@ internal class FloconFilesPluginImpl( } Protocol.ToDevice.Files.Method.DeleteFiles -> { - val deleteFilesMessage = - ToDeviceDeleteFilesMessage.fromJson(message = body) ?: return + val deleteFilesMessage = encoder.decode(body) ?: return fileDataSource.deleteFiles( path = deleteFilesMessage.filePaths, @@ -137,9 +140,7 @@ internal class FloconFilesPluginImpl( } Protocol.ToDevice.Files.Method.DeleteFolderContent -> { - val deleteFolderContentMessage = - ToDeviceDeleteFolderContentMessage.fromJson(message = body) - ?: return + val deleteFolderContentMessage = encoder.decode(body) ?: return fileDataSource.getFile( path = deleteFolderContentMessage.path, @@ -164,7 +165,7 @@ internal class FloconFilesPluginImpl( isConstantPath: Boolean, requestId: String, ) { - val files = fileDataSource.getFolderContent( + fileDataSource.getFolderContent( path = path, isConstantPath = isConstantPath, withFoldersSize = withFoldersSize.value, diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt index 1d4400323..768ac68de 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt @@ -1,15 +1,9 @@ package io.github.openflocon.flocon.plugins.files.model.fromdevice -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable internal data class FilesResultDataModel( val requestId: String, val files: List, -) { - fun toJson(): String { - return FloconEncoder.json.encodeToString(this) - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt index d8e161301..9332cca48 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt @@ -1,7 +1,5 @@ package io.github.openflocon.flocon.plugins.files.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable @@ -10,16 +8,5 @@ internal data class ToDeviceDeleteFileMessage( val parentPath: String, val filePath: String, val isConstantParentPath: Boolean, // ex: context.files / context.caches -) { - companion object { - fun fromJson(message: String): ToDeviceDeleteFileMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt index 465d374d5..757dddda7 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt @@ -1,7 +1,5 @@ package io.github.openflocon.flocon.plugins.files.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable @@ -10,15 +8,4 @@ internal data class ToDeviceDeleteFilesMessage( val parentPath: String, val filePaths: List, val isConstantParentPath: Boolean, // ex: context.files / context.caches -) { - companion object { - fun fromJson(message: String): ToDeviceDeleteFilesMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt index a2eebd4d2..677eab215 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt @@ -1,7 +1,5 @@ package io.github.openflocon.flocon.plugins.files.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable @@ -9,16 +7,5 @@ internal data class ToDeviceDeleteFolderContentMessage( val requestId: String, val path: String, val isConstantPath: Boolean, // ex: context.files / context.caches -) { - companion object { - fun fromJson(message: String): ToDeviceDeleteFolderContentMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt index 7e95a9c28..6c4ae0260 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt @@ -1,22 +1,9 @@ package io.github.openflocon.flocon.plugins.files.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable internal data class ToDeviceGetFileMessage( val requestId: String, val path: String, -) { - companion object { - fun fromJson(message: String): ToDeviceGetFileMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt index d8ba615d7..97774eb4a 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt @@ -1,7 +1,5 @@ package io.github.openflocon.flocon.plugins.files.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable @@ -10,18 +8,6 @@ internal data class ToDeviceGetFilesMessage( val path: String, val isConstantPath: Boolean, // ex: context.files / context.caches val withFoldersSize: Boolean = false, -) { - companion object { - fun fromJson(message: String): ToDeviceGetFilesMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt index 79ba0dd4d..60a90ebe8 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -7,6 +7,7 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconSharedPreferenceModel @@ -22,7 +23,8 @@ object FloconPreferences : FloconPluginFactory): String { - val value = items.map { PreferencesDescriptor(it.name) } - return FloconEncoder.json.encodeToString(value) -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt index a7f90e476..76ece2ed6 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt @@ -1,16 +1,10 @@ package io.github.openflocon.flocon.plugins.sharedprefs.model.fromdevice -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable internal data class SharedPreferenceValueResultDataModel( val requestId: String, val sharedPreferenceName: String, val rows: List, -) { - fun toJson(): String { - return FloconEncoder.json.encodeToString(this) - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt index 30023f510..14c33a941 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt @@ -1,7 +1,5 @@ package io.github.openflocon.flocon.plugins.sharedprefs.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable @@ -15,15 +13,4 @@ internal data class ToDeviceEditSharedPreferenceValueMessage( val booleanValue: Boolean? = null, val longValue: Long? = null, val setStringValue: Set? = null, -) { - companion object { - fun fromJson(jsonString: String): ToDeviceEditSharedPreferenceValueMessage? { - return try { - FloconEncoder.json.decodeFromString(jsonString) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt index b30a6a43c..daaa128fa 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt @@ -1,22 +1,9 @@ package io.github.openflocon.flocon.plugins.sharedprefs.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable internal data class ToDeviceGetSharedPreferenceValueMessage( val requestId: String, val sharedPreferenceName: String, -) { - companion object { - fun fromJson(message: String): ToDeviceGetSharedPreferenceValueMessage? { - return try { - return FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt index d549c1ced..00d03f9a2 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt @@ -1,21 +1,8 @@ package io.github.openflocon.flocon.plugins.sharedprefs.model.todevice -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable @Serializable internal data class ToDeviceGetSharedPrefsMessage( val requestId: String, -) { - companion object { - fun fromJson(message: String): ToDeviceGetSharedPrefsMessage? { - return try { - FloconEncoder.json.decodeFromString(message) - } catch (t: Throwable) { - FloconLogger.logError("parsing issue", t) - null - } - } - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt deleted file mode 100644 index 63f2a342e..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.analytics - -import io.github.openflocon.flocon.FloconApp -import io.github.openflocon.flocon.FloconConfig -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.FloconPluginFactory -import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem - -class FloconAnalyticsConfig : FloconPluginConfig - -/** - * Flocon Analytics Plugin. - */ -object FloconAnalytics : FloconPluginFactory { - override fun createConfig(context: FloconContext) = TODO() - override fun install( - pluginConfig: FloconAnalyticsConfig, - floconConfig: FloconConfig - ): FloconAnalyticsPlugin = TODO() - - override val name: String = "" - override val pluginId: String = "ANALYTICS" -} -// -//fun floconAnalytics(analyticsName: String) : AnalyticsBuilder { -// return AnalyticsBuilder( -// analyticsTableId = analyticsName, -// analyticsPlugin = FloconApp.instance?.client?.analyticsPlugin, -// ) -//} - -//fun FloconApp.analytics(analyticsName: String): AnalyticsBuilder { -// return AnalyticsBuilder( -// analyticsTableId = analyticsName, -// analyticsPlugin = this.client?.analyticsPlugin, -// ) -//} - -interface FloconAnalyticsPlugin : FloconPlugin { - fun registerAnalytics(analyticsItems: List) -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt deleted file mode 100644 index f59c51620..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt +++ /dev/null @@ -1,32 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package io.github.openflocon.flocon.pluginsold.analytics.builder - -import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsEvent -import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem -import io.github.openflocon.flocon.utils.currentTimeMillis -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid - -class AnalyticsBuilder( - val analyticsTableId: String, - private val analyticsPlugin: FloconAnalyticsPlugin?, -) { - fun logEvents(vararg events: AnalyticsEvent) { - this.logEvents(events.toList()) - } - - fun logEvents(events: List) { - val analyticsItems = events.map { - AnalyticsItem( - id = Uuid.random().toString(), - analyticsTableId = analyticsTableId, - eventName = it.eventName, - createdAt = currentTimeMillis(), - properties = it.properties, - ) - } - analyticsPlugin?.registerAnalytics(analyticsItems) - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt deleted file mode 100644 index 10a608249..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.analytics.model - -data class AnalyticsEvent( - val eventName: String, - val properties: List, -) { - constructor( - eventName: String, - vararg properties: AnalyticsPropertiesConfig, - ) : this(eventName, properties.toList()) -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt deleted file mode 100644 index de6d93092..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.analytics.model - -data class AnalyticsPropertiesConfig( - val name: String, - val value: String, -) - -infix fun String.analyticsProperty(value: String) = AnalyticsPropertiesConfig( - name = this, - value = value, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt deleted file mode 100644 index c23f13409..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.analytics.model - -data class AnalyticsItem( - val id: String, - val analyticsTableId: String, - val eventName: String, - val createdAt: Long, - val properties: List, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt index c0b9cfa2b..47aecce92 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/device/FloconDevicePlugin.kt @@ -1,6 +1,11 @@ package io.github.openflocon.flocon.pluginsold.device -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.core.FloconEncoder class FloconDeviceConfig : FloconPluginConfig @@ -14,7 +19,8 @@ object FloconDevice : FloconPluginFactory() @@ -22,7 +22,8 @@ object FloconFiles : FloconPluginFactory { override fun install( pluginConfig: FloconFilesConfig, - floconConfig: FloconConfig + floconConfig: FloconConfig, + encoder: FloconEncoder ): FloconFilesPlugin { TODO("Not yet implemented") } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt index dec9be9dc..f9d06c388 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt @@ -1,11 +1,11 @@ package io.github.openflocon.flocon.pluginsold.sharedprefs -import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconSharedPreferenceModel class FloconPreferencesConfig : FloconPluginConfig @@ -21,7 +21,8 @@ object FloconPreferences : FloconPluginFactory { - override fun createConfig(context: FloconContext): FloconTableConfig { - TODO("Not yet implemented") - } - - override fun install( - pluginConfig: FloconTableConfig, - floconConfig: FloconConfig - ): FloconTablePlugin { - TODO("Not yet implemented") - } - - override val name: String - get() = TODO("Not yet implemented") - override val pluginId: String - get() = TODO("Not yet implemented") -} - -//fun floconTable(tableName: String) : TableBuilder { -// return TableBuilder( -// tableId = tableName, -// tablePlugin = FloconApp.instance?.client?.tablePlugin, -// ) -//} -// -//fun FloconApp.table(tableName: String): TableBuilder { -// return TableBuilder( -// tableId = tableName, -// tablePlugin = this.client?.tablePlugin, -// ) -//} - -interface FloconTablePlugin : FloconPlugin { - fun registerItems(tableItems: List) -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt deleted file mode 100644 index 484c85387..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt +++ /dev/null @@ -1,20 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package io.github.openflocon.flocon.pluginsold.tables.builder - -import kotlin.uuid.ExperimentalUuidApi - -//class TableBuilder( -// val tableName: String, -// private val tablePlugin: FloconTablePlugin?, -//) { -// fun log(vararg columns: TableColumnConfig) { -// val dashboardConfig = TableItem( -// id = Uuid.random().toString(), -// name = tableName, -// columns = columns.toList(), -// createdAt = currentTimeMillis(), -// ) -// tablePlugin?.registerTable(dashboardConfig) -// } -//} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableColumnConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableColumnConfig.kt deleted file mode 100644 index 857f2d8a8..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableColumnConfig.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.tables.model - -data class TableColumnConfig( - val columnName: String, - val value: String, -) - -infix fun String.toParam(value: String) = TableColumnConfig( - columnName = this, - value = value, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableItem.kt deleted file mode 100644 index ba4485e34..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/model/TableItem.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.tables.model - -data class TableItem( - val id: String, - val name: String, - val createdAt: Long, - val columns: List, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt index 73cb6ac63..bdf02ebb7 100644 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt @@ -5,7 +5,6 @@ import androidx.sqlite.driver.NativeSQLiteDriver import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel import io.github.openflocon.flocon.plugins.database.model.FloconFileDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel import platform.Foundation.NSFileManager import platform.posix.close diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.jvm.kt index 01708953c..dc49389d3 100644 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.jvm.kt +++ b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.jvm.kt @@ -3,7 +3,6 @@ package io.github.openflocon.flocon.plugins.database import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel import io.github.openflocon.flocon.plugins.database.model.FloconFileDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel import java.io.File import java.sql.Connection diff --git a/FloconAndroid/gradle/gradle-daemon-jvm.properties b/FloconAndroid/gradle/gradle-daemon-jvm.properties new file mode 100644 index 000000000..cfd299ab0 --- /dev/null +++ b/FloconAndroid/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/584d2f01a3c6e59ebb9478a182f5f714/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/7352c4c0c11b2db21fdd7541204de287/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/584d2f01a3c6e59ebb9478a182f5f714/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/7352c4c0c11b2db21fdd7541204de287/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/12ed6bcbab330f7afa37d16220b272a3/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/a7e1d8e6e800a81047d4aec26156ef5c/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/584d2f01a3c6e59ebb9478a182f5f714/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/7352c4c0c11b2db21fdd7541204de287/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/3676ee7aa5095d7f22645eb0f22ca159/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/fc98f2d434c5796fe6ec02f0f22957b3/redirect +toolchainVendor=AZUL +toolchainVersion=17 diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index e0c4d0368..2fba6fb4b 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -4,7 +4,7 @@ apollo = "4.0.0" coilCompose = "3.2.0" compose = "1.9.0" datastorePreferences = "1.1.7" -kotlin = "2.1.0" +kotlin = "2.1.21" mavenPublish = "0.34.0" coreKtx = "1.16.0" junit = "4.13.2" @@ -26,7 +26,7 @@ grpc = "1.73.0" protobufPlugin = "0.9.5" grpcKotlin = "1.4.3" protobuf = "4.26.1" -ksp = "2.1.0-1.0.29" +ksp = "2.1.21-2.0.2" processPhoenix = "3.0.0" sqlite = "2.6.2" sqliteJdbc = "3.50.3.0" diff --git a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts index fb3e112e3..e6a51bc83 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts @@ -1,30 +1,30 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id("flocon.android.library") - id("flocon.publish") -} - - -android { - namespace = "io.github.openflocon.flocon.grpc.base" -} - - -dependencies { - implementation(projects.flocon) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.kotlinx.coroutines.core) - - implementation(libs.grpc.android) -} - - -mavenPublishing { - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-grpc-interceptor-base", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("flocon.android.library") + id("flocon.publish") +} + + +android { + namespace = "io.github.openflocon.flocon.grpc.base" +} + + +dependencies { + implementation(projects.flocon) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.grpc.android) +} + + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-grpc-interceptor-base", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) } \ No newline at end of file diff --git a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.android.kt b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.android.kt index 5c424d559..844d10f8a 100644 --- a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.android.kt +++ b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.android.kt @@ -1,7 +1,12 @@ package io.github.openflocon.flocon.network.core.datasource import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.core.FloconEncoder internal actual inline fun buildFloconNetworkDataSource( - context: FloconContext -): FloconNetworkDataSource = FloconNetworkDataSourceAndroid(context = context.context) \ No newline at end of file + context: FloconContext, + encoder: FloconEncoder +): FloconNetworkDataSource = FloconNetworkDataSourceAndroid( + context = context.context, + encoder = encoder +) \ No newline at end of file diff --git a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt index 07a3bd15a..7c763823d 100644 --- a/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt +++ b/FloconAndroid/network/core/src/androidMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceAndroid.kt @@ -2,25 +2,25 @@ package io.github.openflocon.flocon.network.core.datasource import android.content.Context import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig -import io.github.openflocon.flocon.network.core.mapper.parseMockResponses -import io.github.openflocon.flocon.network.core.mapper.toJsonString -import io.github.openflocon.flocon.network.core.mapper.writeMockResponsesToJson -import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_BAD_CONFIG_JSON -import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_MOCKS_JSON +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.core.decode +import io.github.openflocon.flocon.core.encode import io.github.openflocon.flocon.network.core.model.BadQualityConfig import io.github.openflocon.flocon.network.core.model.MockNetworkResponse +import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_BAD_CONFIG_JSON +import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_MOCKS_JSON import java.io.File import java.io.FileInputStream import java.io.FileOutputStream internal class FloconNetworkDataSourceAndroid( - private val context: Context + private val context: Context, + private val encoder: FloconEncoder ) : FloconNetworkDataSource { override fun saveMocksToFile(mocks: List) { try { val file = File(context.filesDir, FLOCON_NETWORK_MOCKS_JSON) - val jsonString = writeMockResponsesToJson(mocks = mocks) + val jsonString = encoder.encode(mocks) FileOutputStream(file).use { it.write(jsonString.toByteArray()) } @@ -39,7 +39,9 @@ internal class FloconNetworkDataSourceAndroid( val jsonString = FileInputStream(file).use { it.readBytes().toString(Charsets.UTF_8) } - parseMockResponses(jsonString = jsonString) + + encoder.decode>(jsonString) + .orEmpty() } catch (t: Throwable) { FloconLogger.logError("issue in loadMocksFromFile", t) emptyList() @@ -56,7 +58,8 @@ internal class FloconNetworkDataSourceAndroid( val jsonString = FileInputStream(file).use { it.readBytes().toString(Charsets.UTF_8) } - parseBadQualityConfig(jsonString = jsonString) + + encoder.decode(jsonString) } catch (t: Throwable) { FloconLogger.logError("issue in loadBadNetworkConfig", t) null @@ -69,7 +72,7 @@ internal class FloconNetworkDataSourceAndroid( if (config == null) { file.delete() } else { - val jsonString = config.toJsonString() + val jsonString = encoder.encode(config) FileOutputStream(file).use { it.write(jsonString.toByteArray()) } diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt index a9eac6957..0484e2975 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/FloconNetwork.kt @@ -7,6 +7,7 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.error.pluginNotInitialized @@ -50,12 +51,14 @@ object FloconNetwork : FloconPluginFactory( - toSerializable() - ) -} - -internal fun parseBadQualityConfig(jsonString: String): BadQualityConfig? { - return try { - val parsed = FloconEncoder.json.decodeFromString( - jsonString - ) - parsed.toDomain() - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "bad connection network parsing issue", t) - null - } -} @Serializable internal class BadQualityConfigSerializable( diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt index 329115850..666a358f2 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/FloconNetworkRequestToJson.kt @@ -2,12 +2,10 @@ package io.github.openflocon.flocon.network.core.mapper -import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse import io.github.openflocon.flocon.network.core.model.FloconWebSocketEvent import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -25,20 +23,17 @@ internal class FloconNetworkCallRequestRemote( val requestSize: Long?, ) -internal fun FloconNetworkCallRequest.floconNetworkCallRequestToJson(): String { - val remoteModel = FloconNetworkCallRequestRemote( - floconCallId = floconCallId, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - url = request.url, - method = request.method, - startTime = request.startTime, - requestBody = request.body, - requestHeaders = request.headers, - requestSize = request.size - ) - return FloconEncoder.json.encodeToString(remoteModel) -} +internal fun FloconNetworkCallRequest.floconNetworkCallRequestToJson() = FloconNetworkCallRequestRemote( + floconCallId = floconCallId, + floconNetworkType = floconNetworkType, + isMocked = isMocked, + url = request.url, + method = request.method, + startTime = request.startTime, + requestBody = request.body, + requestHeaders = request.headers, + requestSize = request.size +) @Serializable internal class FloconNetworkCallResponseRemote( @@ -57,27 +52,23 @@ internal class FloconNetworkCallResponseRemote( val isImage: Boolean, ) -internal fun FloconNetworkCallResponse.floconNetworkCallResponseToJson(): String { - val remoteModel = FloconNetworkCallResponseRemote( - floconCallId = floconCallId, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - durationMs = durationMs, - responseHttpCode = response.httpCode, - responseGrpcStatus = response.grpcStatus, - responseContentType = response.contentType, - responseBody = response.body, - responseHeaders = response.headers, - requestHeaders = response.requestHeaders?.takeIf { - it.isNotEmpty() - }, - responseSize = response.size, - isImage = response.isImage, - responseError = response.error, - ) - - return FloconEncoder.json.encodeToString(remoteModel) -} +internal fun FloconNetworkCallResponse.floconNetworkCallResponseToJson() = FloconNetworkCallResponseRemote( + floconCallId = floconCallId, + floconNetworkType = floconNetworkType, + isMocked = isMocked, + durationMs = durationMs, + responseHttpCode = response.httpCode, + responseGrpcStatus = response.grpcStatus, + responseContentType = response.contentType, + responseBody = response.body, + responseHeaders = response.headers, + requestHeaders = response.requestHeaders?.takeIf { + it.isNotEmpty() + }, + responseSize = response.size, + isImage = response.isImage, + responseError = response.error, +) @Serializable internal class FloconWebSocketEventRemote( @@ -90,22 +81,19 @@ internal class FloconWebSocketEventRemote( val error: String?, ) -internal fun FloconWebSocketEvent.floconNetworkWebSocketEventToJson(): String { - val remoteModel = FloconWebSocketEventRemote( - id = Uuid.random().toString(), - event = when (event) { - FloconWebSocketEvent.Event.Closed -> "closed" - FloconWebSocketEvent.Event.Closing -> "closing" - FloconWebSocketEvent.Event.Error -> "error" - FloconWebSocketEvent.Event.ReceiveMessage -> "received" - FloconWebSocketEvent.Event.SendMessage -> "sent" - FloconWebSocketEvent.Event.Open -> "open" - }, - url = websocketUrl, - size = size, - timestamp = timeStamp, - message = message, - error = error?.message - ) - return FloconEncoder.json.encodeToString(remoteModel) -} \ No newline at end of file +internal fun FloconWebSocketEvent.floconNetworkWebSocketEventToJson() = FloconWebSocketEventRemote( + id = Uuid.random().toString(), + event = when (event) { + FloconWebSocketEvent.Event.Closed -> "closed" + FloconWebSocketEvent.Event.Closing -> "closing" + FloconWebSocketEvent.Event.Error -> "error" + FloconWebSocketEvent.Event.ReceiveMessage -> "received" + FloconWebSocketEvent.Event.SendMessage -> "sent" + FloconWebSocketEvent.Event.Open -> "open" + }, + url = websocketUrl, + size = size, + timestamp = timeStamp, + message = message, + error = error?.message +) \ No newline at end of file diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt index 76b00f959..dc3cf7079 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt @@ -28,20 +28,6 @@ internal class MockNetworkResponseDataModel( ) } - -internal fun parseMockResponses(jsonString: String): List { - try { - val remote = - FloconEncoder.json.decodeFromString>(jsonString) - return remote.mapNotNull { - it.toDomain() - } - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "mock network parsing issue", t) - return emptyList() - } -} - internal fun MockNetworkResponseDataModel.toDomain(): MockNetworkResponse? { return MockNetworkResponse( expectation = MockNetworkResponse.Expectation( @@ -76,16 +62,6 @@ private fun MockNetworkResponseDataModel.mapResponseToDomain(): MockNetworkRespo } } - -internal fun writeMockResponsesToJson(mocks: List): String { - return try { - FloconEncoder.json.encodeToString(mocks.map { it.toRemote() }) - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "mock network writing issue", t) - return "[]" - } -} - private fun MockNetworkResponse.toRemote(): MockNetworkResponseDataModel { return MockNetworkResponseDataModel( expectation = MockNetworkResponseDataModel.Expectation( diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt index 8eeaeb931..967ed6ff1 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt @@ -1,25 +1,9 @@ package io.github.openflocon.flocon.network.core.mapper -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString @Serializable internal class WebSocketMockMessage( val id: String, val message: String, -) - -internal fun webSocketIdsToJsonArray(ids: Collection): String { - return FloconEncoder.json.encodeToString(ids) -} - -internal fun parseWebSocketMockMessage(jsonString: String): WebSocketMockMessage? { - try { - return FloconEncoder.json.decodeFromString(jsonString) - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "mock wesocket network parsing issue", t) - } - return null -} \ No newline at end of file +) \ No newline at end of file diff --git a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt index c95e76a53..15b43004d 100644 --- a/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/plugin/FloconNetworkPluginImpl.kt @@ -4,16 +4,16 @@ import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.decode +import io.github.openflocon.flocon.core.encode +import io.github.openflocon.flocon.network.core.FloconNetworkPlugin import io.github.openflocon.flocon.network.core.datasource.buildFloconNetworkDataSource +import io.github.openflocon.flocon.network.core.mapper.WebSocketMockMessage import io.github.openflocon.flocon.network.core.mapper.floconNetworkCallRequestToJson import io.github.openflocon.flocon.network.core.mapper.floconNetworkCallResponseToJson import io.github.openflocon.flocon.network.core.mapper.floconNetworkWebSocketEventToJson -import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig -import io.github.openflocon.flocon.network.core.mapper.parseMockResponses -import io.github.openflocon.flocon.network.core.mapper.parseWebSocketMockMessage -import io.github.openflocon.flocon.network.core.mapper.webSocketIdsToJsonArray -import io.github.openflocon.flocon.network.core.FloconNetworkPlugin import io.github.openflocon.flocon.network.core.model.BadQualityConfig import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse @@ -32,11 +32,12 @@ internal const val FLOCON_NETWORK_BAD_CONFIG_JSON = "flocon_network_bad_config.j internal class FloconNetworkPluginImpl( context: FloconContext, private var sender: FloconMessageSender, - private val coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope, + private val encoder: FloconEncoder ) : FloconPlugin, FloconNetworkPlugin { override val key: String = "NETWORK" - private val dataSource = buildFloconNetworkDataSource(context) + private val dataSource = buildFloconNetworkDataSource(context = context, encoder = encoder) private val websocketListeners = MutableStateFlow>(emptyMap()) @@ -56,7 +57,7 @@ internal class FloconNetworkPluginImpl( sender.send( plugin = Protocol.FromDevice.Network.Plugin, method = Protocol.FromDevice.Network.Method.LogNetworkCallRequest, - body = request.floconNetworkCallRequestToJson(), + body = encoder.encode(request.floconNetworkCallRequestToJson()) ) } } catch (t: Throwable) { @@ -71,7 +72,7 @@ internal class FloconNetworkPluginImpl( sender.send( plugin = Protocol.FromDevice.Network.Plugin, method = Protocol.FromDevice.Network.Method.LogNetworkCallResponse, - body = response.floconNetworkCallResponseToJson(), + body = encoder.encode(response.floconNetworkCallResponseToJson()) ) } catch (t: Throwable) { FloconLogger.logError("Network json mapping error", t) @@ -87,7 +88,7 @@ internal class FloconNetworkPluginImpl( sender.send( plugin = Protocol.FromDevice.Network.Plugin, method = Protocol.FromDevice.Network.Method.LogWebSocketEvent, - body = event.floconNetworkWebSocketEventToJson(), + body = encoder.encode(event.floconNetworkWebSocketEventToJson()) ) } catch (t: Throwable) { FloconLogger.logError("Network json mapping error", t) @@ -101,19 +102,20 @@ internal class FloconNetworkPluginImpl( ) { when (method) { Protocol.ToDevice.Network.Method.SetupMocks -> { - val setup = parseMockResponses(jsonString = body) + val setup = encoder.decode>(body) + .orEmpty() _mocks.update { setup } dataSource.saveMocksToFile(mocks) } Protocol.ToDevice.Network.Method.SetupBadNetworkConfig -> { - val config = parseBadQualityConfig(jsonString = body) + val config = encoder.decode(body) _badQualityConfig.update { config } dataSource.saveBadNetworkConfig(config) } Protocol.ToDevice.Network.Method.WebsocketMockMessage -> { - val message = parseWebSocketMockMessage(jsonString = body) + val message = encoder.decode(body) if (message != null) { websocketListeners.value[message.id]?.onMessage(message.message) } @@ -135,11 +137,11 @@ internal class FloconNetworkPluginImpl( updateWebSocketIds() } - private suspend fun updateWebSocketIds() { + private fun updateWebSocketIds() { sender.send( plugin = Protocol.FromDevice.Network.Plugin, method = Protocol.FromDevice.Network.Method.RegisterWebSocketIds, - body = webSocketIdsToJsonArray(ids = websocketListeners.value.keys), + body = encoder.encode(websocketListeners.value.keys) ) } diff --git a/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts b/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts index aa77a7815..094f66831 100644 --- a/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/network/okhttp-interceptor-no-op/build.gradle.kts @@ -1,25 +1,22 @@ -plugins { - id("flocon.android.library") - id("flocon.publish") -} - - -android { - namespace = "io.github.openflocon.flocon.okhttp" -} - - -dependencies { - implementation(projects.network.coreNoOp) - implementation(platform(libs.okhttp.bom)) - implementation(libs.okhttp) -} - - -mavenPublishing { - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-okhttp-interceptor-no-op", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) -} \ No newline at end of file +plugins { + id("flocon.android.library") + id("flocon.publish") +} + +android { + namespace = "io.github.openflocon.flocon.okhttp" +} + +dependencies { + implementation(projects.network.coreNoOp) + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-okhttp-interceptor-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/network/okhttp-interceptor/build.gradle.kts b/FloconAndroid/network/okhttp-interceptor/build.gradle.kts index 7816ec3d0..ca055d13d 100644 --- a/FloconAndroid/network/okhttp-interceptor/build.gradle.kts +++ b/FloconAndroid/network/okhttp-interceptor/build.gradle.kts @@ -1,32 +1,30 @@ -plugins { - id("flocon.android.library") - id("flocon.publish") -} - -android { - namespace = "io.github.openflocon.flocon.okhttp" -} - -dependencies { - - api(projects.network.core) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.android) - - implementation(platform(libs.okhttp.bom)) - implementation(libs.okhttp) - implementation(libs.brotli.dec) - - testImplementation(libs.junit) -} - - -mavenPublishing { - coordinates( - groupId = project.property("floconGroupId") as String, - artifactId = "flocon-okhttp-interceptor", - version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String - ) -} \ No newline at end of file +plugins { + id("flocon.android.library") + id("flocon.publish") +} + +android { + namespace = "io.github.openflocon.flocon.okhttp" +} + +dependencies { + api(projects.network.core) + + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) + implementation(libs.brotli.dec) + + testImplementation(libs.junit) +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-okhttp-interceptor", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index 9d077af4c..05691da2a 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -88,6 +88,9 @@ dependencies { debugImplementation(projects.deeplinks) releaseImplementation(projects.deeplinksNoOp) + debugImplementation(projects.tables) + releaseImplementation(projects.tablesNoOp) + debugImplementation(project(":database:room")) releaseImplementation(project(":database:room-no-op")) debugImplementation(project(":database:room3")) diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index acc9d0f03..4c26b2858 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -22,18 +22,20 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.database.core.FloconDatabase -import io.github.openflocon.flocon.database.room.floconRegisterDatabase import io.github.openflocon.flocon.database.room.room import io.github.openflocon.flocon.myapplication.database.DogDatabase import io.github.openflocon.flocon.myapplication.database.initializeDatabases import io.github.openflocon.flocon.myapplication.database.initializeInMemoryDatabases import io.github.openflocon.flocon.myapplication.database.model.DogEntity import io.github.openflocon.flocon.myapplication.grpc.GrpcController +import io.github.openflocon.flocon.myapplication.table.initializeTable import io.github.openflocon.flocon.myapplication.ui.ImagesListView import io.github.openflocon.flocon.myapplication.ui.theme.MyApplicationTheme import io.github.openflocon.flocon.network.core.FloconNetwork import io.github.openflocon.flocon.okhttp.FloconOkhttpInterceptor +import io.github.openflocon.flocon.plugins.analytics.FloconAnalytics import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks +import io.github.openflocon.flocon.plugins.tables.FloconTable import io.github.openflocon.flocon.startFlocon import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -87,7 +89,7 @@ class MainActivity : ComponentActivity() { // val graphQlTester = GraphQlTester(client = okHttpClient) // initializeImages(context = this, okHttpClient = okHttpClient) // initializeDashboard(this) -// initializeTable(this) + initializeTable() setContent { MyApplicationTheme { @@ -223,9 +225,7 @@ class MainActivity : ComponentActivity() { install(FloconDeeplinks) { deeplink("flocon://home") deeplink("flocon://test") - deeplink( - "flocon://user/[userId]" - ) { + deeplink("flocon://user/[userId]") { label = "User" "userId" withAutoComplete listOf("Florent", "David", "Guillaume") } @@ -236,6 +236,8 @@ class MainActivity : ComponentActivity() { } install(FloconNetwork) + install(FloconTable) + install(FloconAnalytics) install(FloconDatabase) { room() } diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeDashboard.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeDashboard.kt deleted file mode 100644 index 7a5050f0c..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeDashboard.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.myapplication.table - -import android.content.Context - -fun initializeTable(context: Context) { -// Flocon.table("analytics").log( -// "name" toParam "nameValue", -// "value1" toParam "value1Value", -// "value2" toParam "value2Value", -// ) -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt new file mode 100644 index 000000000..067f69081 --- /dev/null +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt @@ -0,0 +1,13 @@ +package io.github.openflocon.flocon.myapplication.table + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.plugins.tables.dsl.table +import io.github.openflocon.flocon.plugins.tables.tablePlugin + +fun initializeTable() { + Flocon.tablePlugin.table("analytics") { + column("name", "nameValue") + column("value1", "value1Value") + column("value2", "value2Value") + } +} \ No newline at end of file diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index 2521b305b..3f81b8483 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -9,6 +9,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) @@ -34,6 +37,8 @@ include(":datastores") include(":datastores-no-op") include(":deeplinks") include(":deeplinks-no-op") +include(":tables") +include(":tables-no-op") include(":network:core") include(":network:core-no-op") include(":database:core") diff --git a/FloconAndroid/tables-no-op/build.gradle.kts b/FloconAndroid/tables-no-op/build.gradle.kts new file mode 100644 index 000000000..60335b38d --- /dev/null +++ b/FloconAndroid/tables-no-op/build.gradle.kts @@ -0,0 +1,117 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "io.github.openflocon.flocon.tables.noop" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-tables-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Tables No-Op" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesNoOp.kt b/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesNoOp.kt new file mode 100644 index 000000000..49946f2fc --- /dev/null +++ b/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesNoOp.kt @@ -0,0 +1,43 @@ +package io.github.openflocon.flocon.plugins.tables + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.plugins.tables.model.TableItem + +interface FloconTablePlugin : FloconPlugin { + fun registerItems(tableItems: List) +} + +class FloconTableConfig internal constructor() : FloconPluginConfig + +object FloconTable : FloconPluginFactory { + override val name: String = "Table" + override val pluginId: String = Protocol.ToDevice.Table.Plugin + override fun createConfig(context: FloconContext) = FloconTableConfig() + override fun install( + pluginConfig: FloconTableConfig, + floconConfig: FloconConfig + ): FloconTablePlugin { + return FloconTablePluginNoOp + } +} + +private object FloconTablePluginNoOp : FloconTablePlugin { + override val key: String = "TABLE" + + override suspend fun onMessageReceived(method: String, body: String) { + // no-op + } + + override suspend fun onConnectedToServer() { + // no-op + } + + override fun registerItems(tableItems: List) { + // no-op + } +} diff --git a/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt b/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt new file mode 100644 index 000000000..bbe3ca64c --- /dev/null +++ b/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt @@ -0,0 +1,4 @@ +package io.github.openflocon.flocon.plugins.tables.model + +class TableItem { +} \ No newline at end of file diff --git a/FloconAndroid/tables/build.gradle.kts b/FloconAndroid/tables/build.gradle.kts new file mode 100644 index 000000000..4b6551370 --- /dev/null +++ b/FloconAndroid/tables/build.gradle.kts @@ -0,0 +1,119 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.serialization.json) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "io.github.openflocon.flocon.tables" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-tables", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Tables" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt similarity index 62% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt rename to FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt index 8c63f218d..251372092 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt @@ -1,5 +1,6 @@ package io.github.openflocon.flocon.plugins.tables +import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger @@ -7,9 +8,13 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.encode +import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.error.pluginNotInitialized import io.github.openflocon.flocon.plugins.tables.model.TableItem -import io.github.openflocon.flocon.plugins.tables.model.tableItemListToJson +import io.github.openflocon.flocon.plugins.tables.model.toRemote class FloconTableConfig : FloconPluginConfig @@ -23,16 +28,24 @@ object FloconTable : FloconPluginFactory { override fun createConfig(context: FloconContext) = FloconTableConfig() override fun install( pluginConfig: FloconTableConfig, - floconConfig: FloconConfig + floconConfig: FloconConfig, + encoder: FloconEncoder ): FloconTablePlugin { return FloconTablePluginImpl( - sender = floconConfig.client as FloconMessageSender + sender = floconConfig.client as FloconMessageSender, + encoder = encoder ) + .also { FloconTablePluginImpl.plugin = it } } } +@OptIn(FloconMarker::class) +val Flocon.Companion.tablePlugin: FloconTablePlugin + get() = FloconTablePluginImpl.plugin ?: pluginNotInitialized("table") + internal class FloconTablePluginImpl( private val sender: FloconMessageSender, + private val encoder: FloconEncoder ) : FloconPlugin, FloconTablePlugin { override val key: String = "TABLE" @@ -53,13 +66,17 @@ internal class FloconTablePluginImpl( private fun sendTable(tableItems: List) { try { -// sender.send( -// plugin = Protocol.FromDevice.Table.Plugin, -// method = Protocol.FromDevice.Table.Method.AddItems, -// body = tableItemListToJson(tableItems).toString() -// ) + sender.send( + plugin = Protocol.FromDevice.Table.Plugin, + method = Protocol.FromDevice.Table.Method.AddItems, + body = encoder.encode(tableItems.map(TableItem::toRemote)) + ) } catch (t: Throwable) { FloconLogger.logError("Table json mapping error", t) } } -} \ No newline at end of file + + companion object { + var plugin: FloconTablePlugin? = null + } +} diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt new file mode 100644 index 000000000..caf8fb1a5 --- /dev/null +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt @@ -0,0 +1,35 @@ +@file:OptIn(ExperimentalUuidApi::class, ExperimentalTime::class) + +package io.github.openflocon.flocon.plugins.tables.dsl + +import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin +import io.github.openflocon.flocon.plugins.tables.model.TableColumnConfig +import io.github.openflocon.flocon.plugins.tables.model.TableItem +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +fun FloconTablePlugin.table(tableName: String, block: TableItemDefinition.() -> Unit = {}) { + val item = TableItemDefinition(tableName).apply(block) + .build() + + registerItems(tableItems = listOf(item)) +} + +class TableItemDefinition internal constructor(private val name: String) { + + private val columns: MutableList = mutableListOf() + + fun column(name: String, value: String) { + columns.add(TableColumnConfig(columnName = name, value = value)) + } + + internal fun build() = TableItem( + id = Uuid.random().toHexString(), + name = name, + createdAt = Clock.System.now().toEpochMilliseconds(), + columns = emptyList() + ) + +} \ No newline at end of file diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt new file mode 100644 index 000000000..5d784687a --- /dev/null +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.plugins.tables.model + +data class TableColumnConfig( + val columnName: String, + val value: String, +) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt similarity index 63% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt rename to FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt index 81467899b..187a65732 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt @@ -1,8 +1,6 @@ package io.github.openflocon.flocon.plugins.tables.model -import io.github.openflocon.flocon.core.FloconEncoder import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString data class TableItem( val id: String, @@ -11,22 +9,6 @@ data class TableItem( val columns: List, ) -data class TableColumnConfig( - val columnName: String, - val value: String, -) - -infix fun String.toParam(value: String) = TableColumnConfig( - columnName = this, - value = value, -) - -// --- JSON Serialization --- - -internal fun tableItemListToJson(items: Collection): String { - return FloconEncoder.json.encodeToString(items.map { it.toRemote() }) -} - @Serializable internal class TableItemRemote( val id: String, @@ -53,4 +35,4 @@ internal fun TableItem.toRemote(): TableItemRemote = TableItemRemote( internal fun TableColumnConfig.toRemote(): TableColumnRemote = TableColumnRemote( column = columnName, value = value -) \ No newline at end of file +) From 3ab7cd8f316293fcde0bc8f3a43e89783adeb23a Mon Sep 17 00:00:00 2001 From: Raphael Teyssandier Date: Wed, 13 May 2026 14:17:42 +0200 Subject: [PATCH 24/38] 2.0.0 - Analytics (#511) --- .../analytics-no-op/build.gradle.kts | 98 +++++++++++++++++ .../plugins/analytics/FloconAnalyticsNoOp.kt | 47 ++++++++ .../analytics/builder/AnalyticsBuilder.kt | 17 +++ .../plugins/analytics/model/AnalyticsEvent.kt | 11 ++ .../plugins/analytics/model/AnalyticsItem.kt | 9 ++ .../model/AnalyticsPropertiesConfig.kt | 11 ++ FloconAndroid/analytics/build.gradle.kts | 100 ++++++++++++++++++ .../analytics/FloconAnalyticsPlugin.kt | 67 ++++++++++++ .../analytics/builder/AnalyticsBuilder.kt | 32 ++++++ .../analytics/mapper/AnalyticsItemsMapper.kt | 44 ++++++++ .../plugins/analytics/model/AnalyticsEvent.kt | 11 ++ .../plugins/analytics/model/AnalyticsItem.kt | 9 ++ .../model/AnalyticsPropertiesConfig.kt | 11 ++ .../database/room3-no-op/build.gradle.kts | 3 - .../analytics/FloconAnalyticsPlugin.kt | 44 ++++++++ .../analytics/builder/AnalyticsBuilder.kt | 32 ++++++ .../analytics/model/AnalyticsEvent.kt | 11 ++ .../model/AnalyticsPropertiesConfig.kt | 11 ++ .../pluginsold/analytics/model/TableItem.kt | 9 ++ .../pluginsold/tables/builder/TableBuilder.kt | 20 ++++ .../core/noop/mapper/MockResponseToJson.kt | 4 + .../network/core/noop/mapper/Websocket.kt | 4 + FloconAndroid/tables-no-op/build.gradle.kts | 3 + FloconAndroid/tables/build.gradle.kts | 2 + .../plugins/tables/FloconTablesPlugin.kt | 1 + .../flocon/plugins/tables/dsl/TableItemDsl.kt | 4 +- .../plugins/tables/model/TableColumnConfig.kt | 2 +- .../flocon/plugins/tables/model/TableItem.kt | 3 +- 28 files changed, 613 insertions(+), 7 deletions(-) create mode 100644 FloconAndroid/analytics-no-op/build.gradle.kts create mode 100644 FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsNoOp.kt create mode 100644 FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt create mode 100644 FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt create mode 100644 FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt create mode 100644 FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt create mode 100644 FloconAndroid/analytics/build.gradle.kts create mode 100644 FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt create mode 100644 FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt create mode 100644 FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt create mode 100644 FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt create mode 100644 FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt create mode 100644 FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt diff --git a/FloconAndroid/analytics-no-op/build.gradle.kts b/FloconAndroid/analytics-no-op/build.gradle.kts new file mode 100644 index 000000000..a7e3f4f02 --- /dev/null +++ b/FloconAndroid/analytics-no-op/build.gradle.kts @@ -0,0 +1,98 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.analytics.noop" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-analytics-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Analytics No-Op" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsNoOp.kt b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsNoOp.kt new file mode 100644 index 000000000..3e4204f84 --- /dev/null +++ b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsNoOp.kt @@ -0,0 +1,47 @@ +package io.github.openflocon.flocon.plugins.analytics + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem + +class FloconAnalyticsConfig : FloconPluginConfig + +interface FloconAnalyticsPlugin : FloconPlugin { + fun registerAnalytics(analyticsItems: List) +} + +object FloconAnalytics : FloconPluginFactory { + override val name: String = "Analytics" + override val pluginId: String = Protocol.ToDevice.Analytics.Plugin + override fun createConfig(context: FloconContext) = FloconAnalyticsConfig() + override fun install( + pluginConfig: FloconAnalyticsConfig, + floconConfig: FloconConfig + ): FloconAnalyticsPlugin { + return FloconAnalyticsNoOpImpl() + } +} + +internal class FloconAnalyticsNoOpImpl : FloconPlugin, FloconAnalyticsPlugin { + override val key: String + get() = Protocol.ToDevice.Analytics.Plugin + + override suspend fun onMessageReceived( + method: String, + body: String, + ) { + // no op + } + + override suspend fun onConnectedToServer() { + // no op + } + + override fun registerAnalytics(analyticsItems: List) { + // no op + } +} diff --git a/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt new file mode 100644 index 000000000..2ae74f610 --- /dev/null +++ b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt @@ -0,0 +1,17 @@ +package io.github.openflocon.flocon.plugins.analytics.builder + +import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent + +class AnalyticsBuilder( + val analyticsTableId: String, + private val analyticsPlugin: FloconAnalyticsPlugin?, +) { + fun logEvents(vararg events: AnalyticsEvent) { + // no-op + } + + fun logEvents(events: List) { + // no-op + } +} diff --git a/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt new file mode 100644 index 000000000..b0de88ffa --- /dev/null +++ b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsEvent( + val eventName: String, + val properties: List, +) { + constructor( + eventName: String, + vararg properties: AnalyticsPropertiesConfig, + ) : this(eventName, properties.toList()) +} diff --git a/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt new file mode 100644 index 000000000..55c7285cd --- /dev/null +++ b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsItem( + val id: String, + val analyticsTableId: String, + val eventName: String, + val createdAt: Long, + val properties: List, +) diff --git a/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt new file mode 100644 index 000000000..f36d72382 --- /dev/null +++ b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsPropertiesConfig( + val name: String, + val value: String, +) + +infix fun String.analyticsProperty(value: String) = AnalyticsPropertiesConfig( + name = this, + value = value, +) diff --git a/FloconAndroid/analytics/build.gradle.kts b/FloconAndroid/analytics/build.gradle.kts new file mode 100644 index 000000000..870ee0a77 --- /dev/null +++ b/FloconAndroid/analytics/build.gradle.kts @@ -0,0 +1,100 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(libs.kotlinx.serialization.json) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.analytics" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-analytics", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon Analytics" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt new file mode 100644 index 000000000..9e2fe0f5c --- /dev/null +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -0,0 +1,67 @@ +package io.github.openflocon.flocon.plugins.analytics + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem + +class FloconAnalyticsConfig : FloconPluginConfig + +interface FloconAnalyticsPlugin : FloconPlugin { + fun registerAnalytics(analyticsItems: List) +} + +object FloconAnalytics : FloconPluginFactory { + override val name: String = "Analytics" + override val pluginId: String = Protocol.ToDevice.Analytics.Plugin + override fun createConfig(context: FloconContext) = FloconAnalyticsConfig() + override fun install( + pluginConfig: FloconAnalyticsConfig, + floconConfig: FloconConfig + ): FloconAnalyticsPlugin { + return FloconAnalyticsPluginImpl( + sender = floconConfig.client as FloconMessageSender + ) + } +} + +internal class FloconAnalyticsPluginImpl( + private val sender: FloconMessageSender, +) : FloconPlugin, FloconAnalyticsPlugin { + override val key: String + get() = Protocol.ToDevice.Analytics.Plugin + + override suspend fun onMessageReceived( + method: String, + body: String, + ) { + // no op + } + + override suspend fun onConnectedToServer() { + // no op + } + + override fun registerAnalytics(analyticsItems: List) { + sendAnalytics(analyticsItems) + } + + private fun sendAnalytics(analyticsItems: List) { + analyticsItems.takeIf { it.isNotEmpty() }?.forEach { toSend -> + try { +// sender.send( +// plugin = Protocol.FromDevice.Analytics.Plugin, +// method = Protocol.FromDevice.Analytics.Method.AddItems, +// body = analyticsItemsToJson(toSend) +// ) + } catch (t: Throwable) { + FloconLogger.logError("error on sendAnalytics", t) + } + } + } +} diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt new file mode 100644 index 000000000..d4aef5b4d --- /dev/null +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt @@ -0,0 +1,32 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package io.github.openflocon.flocon.plugins.analytics.builder + +import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.utils.currentTimeMillis +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +class AnalyticsBuilder( + val analyticsTableId: String, + private val analyticsPlugin: FloconAnalyticsPlugin?, +) { + fun logEvents(vararg events: AnalyticsEvent) { + this.logEvents(events.toList()) + } + + fun logEvents(events: List) { + val analyticsItems = events.map { + AnalyticsItem( + id = Uuid.random().toString(), + analyticsTableId = analyticsTableId, + eventName = it.eventName, + createdAt = currentTimeMillis(), + properties = it.properties, + ) + } + analyticsPlugin?.registerAnalytics(analyticsItems) + } +} diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt new file mode 100644 index 000000000..e8ec738ee --- /dev/null +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt @@ -0,0 +1,44 @@ +package io.github.openflocon.flocon.plugins.analytics.mapper + +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString + +internal fun analyticsItemsToJson(item: AnalyticsItem): String { + return FloconEncoder.json.encodeToString( + listOf( + item.toSerializable() + ) + ) +} + +@Serializable +internal class AnalyticsItemSerializable( + val id: String, + val analyticsTableId: String, + val eventName: String, + val createdAt: Long, + val properties: List, +) + +@Serializable +internal class AnalyticsPropertySerializable( + val name: String, + val value: String, +) + +internal fun AnalyticsItem.toSerializable(): AnalyticsItemSerializable { + return AnalyticsItemSerializable( + id = id, + analyticsTableId = analyticsTableId, + eventName = eventName, + createdAt = createdAt, + properties = properties.map { + AnalyticsPropertySerializable( + name = it.name, + value = it.value + ) + } + ) +} diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt new file mode 100644 index 000000000..b0de88ffa --- /dev/null +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsEvent( + val eventName: String, + val properties: List, +) { + constructor( + eventName: String, + vararg properties: AnalyticsPropertiesConfig, + ) : this(eventName, properties.toList()) +} diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt new file mode 100644 index 000000000..55c7285cd --- /dev/null +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsItem( + val id: String, + val analyticsTableId: String, + val eventName: String, + val createdAt: Long, + val properties: List, +) diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt new file mode 100644 index 000000000..f36d72382 --- /dev/null +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.plugins.analytics.model + +data class AnalyticsPropertiesConfig( + val name: String, + val value: String, +) + +infix fun String.analyticsProperty(value: String) = AnalyticsPropertiesConfig( + name = this, + value = value, +) diff --git a/FloconAndroid/database/room3-no-op/build.gradle.kts b/FloconAndroid/database/room3-no-op/build.gradle.kts index 60cdfa68a..6507ac962 100644 --- a/FloconAndroid/database/room3-no-op/build.gradle.kts +++ b/FloconAndroid/database/room3-no-op/build.gradle.kts @@ -67,14 +67,12 @@ android { ) } } - compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } - mavenPublishing { publishToMavenCentral(automaticRelease = true) @@ -90,7 +88,6 @@ mavenPublishing { version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - pom { name = "Flocon Room 3 Implementation No-Op" description = project.property("floconDescription") as String diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt new file mode 100644 index 000000000..e8bd6f068 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt @@ -0,0 +1,44 @@ +package io.github.openflocon.flocon.pluginsold.analytics + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem + +class FloconAnalyticsConfig : FloconPluginConfig + +/** + * Flocon Analytics Plugin. + */ +object FloconAnalytics : FloconPluginFactory { + override fun createConfig(context: FloconContext) = TODO() + override fun install( + pluginConfig: FloconAnalyticsConfig, + floconConfig: FloconConfig, + encoder: FloconEncoder + ): FloconAnalyticsPlugin = TODO() + + override val name: String = "" + override val pluginId: String = "ANALYTICS" +} +// +//fun floconAnalytics(analyticsName: String) : AnalyticsBuilder { +// return AnalyticsBuilder( +// analyticsTableId = analyticsName, +// analyticsPlugin = FloconApp.instance?.client?.analyticsPlugin, +// ) +//} + +//fun FloconApp.analytics(analyticsName: String): AnalyticsBuilder { +// return AnalyticsBuilder( +// analyticsTableId = analyticsName, +// analyticsPlugin = this.client?.analyticsPlugin, +// ) +//} + +interface FloconAnalyticsPlugin : FloconPlugin { + fun registerAnalytics(analyticsItems: List) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt new file mode 100644 index 000000000..f59c51620 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt @@ -0,0 +1,32 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package io.github.openflocon.flocon.pluginsold.analytics.builder + +import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsPlugin +import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsEvent +import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.utils.currentTimeMillis +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +class AnalyticsBuilder( + val analyticsTableId: String, + private val analyticsPlugin: FloconAnalyticsPlugin?, +) { + fun logEvents(vararg events: AnalyticsEvent) { + this.logEvents(events.toList()) + } + + fun logEvents(events: List) { + val analyticsItems = events.map { + AnalyticsItem( + id = Uuid.random().toString(), + analyticsTableId = analyticsTableId, + eventName = it.eventName, + createdAt = currentTimeMillis(), + properties = it.properties, + ) + } + analyticsPlugin?.registerAnalytics(analyticsItems) + } +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt new file mode 100644 index 000000000..10a608249 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.pluginsold.analytics.model + +data class AnalyticsEvent( + val eventName: String, + val properties: List, +) { + constructor( + eventName: String, + vararg properties: AnalyticsPropertiesConfig, + ) : this(eventName, properties.toList()) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt new file mode 100644 index 000000000..de6d93092 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.pluginsold.analytics.model + +data class AnalyticsPropertiesConfig( + val name: String, + val value: String, +) + +infix fun String.analyticsProperty(value: String) = AnalyticsPropertiesConfig( + name = this, + value = value, +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt new file mode 100644 index 000000000..c23f13409 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.pluginsold.analytics.model + +data class AnalyticsItem( + val id: String, + val analyticsTableId: String, + val eventName: String, + val createdAt: Long, + val properties: List, +) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt new file mode 100644 index 000000000..484c85387 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/tables/builder/TableBuilder.kt @@ -0,0 +1,20 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package io.github.openflocon.flocon.pluginsold.tables.builder + +import kotlin.uuid.ExperimentalUuidApi + +//class TableBuilder( +// val tableName: String, +// private val tablePlugin: FloconTablePlugin?, +//) { +// fun log(vararg columns: TableColumnConfig) { +// val dashboardConfig = TableItem( +// id = Uuid.random().toString(), +// name = tableName, +// columns = columns.toList(), +// createdAt = currentTimeMillis(), +// ) +// tablePlugin?.registerTable(dashboardConfig) +// } +//} \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt index 7ab3d09bf..2f96ac6ab 100644 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt @@ -1,4 +1,8 @@ +<<<<<<<< HEAD:FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt +package io.github.openflocon.flocon.network.core.mapper +======== package io.github.openflocon.flocon.network.core.noop.mapper +>>>>>>>> 2.0.0:FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt index 8cd994774..567a072cf 100644 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt @@ -1,4 +1,8 @@ +<<<<<<<< HEAD:FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt +package io.github.openflocon.flocon.network.core.mapper +======== package io.github.openflocon.flocon.network.core.noop.mapper +>>>>>>>> 2.0.0:FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder diff --git a/FloconAndroid/tables-no-op/build.gradle.kts b/FloconAndroid/tables-no-op/build.gradle.kts index 60335b38d..58c5af864 100644 --- a/FloconAndroid/tables-no-op/build.gradle.kts +++ b/FloconAndroid/tables-no-op/build.gradle.kts @@ -68,12 +68,14 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } + mavenPublishing { publishToMavenCentral(automaticRelease = true) @@ -89,6 +91,7 @@ mavenPublishing { version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) + pom { name = "Flocon Tables No-Op" description = project.property("floconDescription") as String diff --git a/FloconAndroid/tables/build.gradle.kts b/FloconAndroid/tables/build.gradle.kts index 4b6551370..f6cb5e71d 100644 --- a/FloconAndroid/tables/build.gradle.kts +++ b/FloconAndroid/tables/build.gradle.kts @@ -30,11 +30,13 @@ kotlin { val androidMain by getting { dependencies { + implementation(libs.brotli.dec) } } val jvmMain by getting { dependencies { + implementation(libs.brotli.dec) } } diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt index 251372092..e71bf2ee7 100644 --- a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt @@ -15,6 +15,7 @@ import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.error.pluginNotInitialized import io.github.openflocon.flocon.plugins.tables.model.TableItem import io.github.openflocon.flocon.plugins.tables.model.toRemote +import kotlin.collections.map class FloconTableConfig : FloconPluginConfig diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt index caf8fb1a5..98802bc8a 100644 --- a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt @@ -3,8 +3,8 @@ package io.github.openflocon.flocon.plugins.tables.dsl import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin -import io.github.openflocon.flocon.plugins.tables.model.TableColumnConfig import io.github.openflocon.flocon.plugins.tables.model.TableItem +import io.github.openflocon.flocon.pluginsold.tables.model.TableColumnConfig import kotlin.time.Clock import kotlin.time.ExperimentalTime import kotlin.uuid.ExperimentalUuidApi @@ -28,7 +28,7 @@ class TableItemDefinition internal constructor(private val name: String) { internal fun build() = TableItem( id = Uuid.random().toHexString(), name = name, - createdAt = Clock.System.now().toEpochMilliseconds(), + createdAt = Clock.System.now().toEpochMilliseconds(), columns = emptyList() ) diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt index 5d784687a..aa1c2f5a7 100644 --- a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.tables.model +package io.github.openflocon.flocon.pluginsold.tables.model data class TableColumnConfig( val columnName: String, diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt index 187a65732..9eaff3359 100644 --- a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt @@ -1,5 +1,6 @@ package io.github.openflocon.flocon.plugins.tables.model +import io.github.openflocon.flocon.pluginsold.tables.model.TableColumnConfig import kotlinx.serialization.Serializable data class TableItem( @@ -29,7 +30,7 @@ internal fun TableItem.toRemote(): TableItemRemote = TableItemRemote( id = id, name = name, createdAt = createdAt, - columns = columns.map { it.toRemote() } + columns = columns.map(TableColumnConfig::toRemote) ) internal fun TableColumnConfig.toRemote(): TableColumnRemote = TableColumnRemote( From 0ad3ca9f161dd06137558273259adbd4f409ef85 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 25 Mar 2026 23:15:16 +0100 Subject: [PATCH 25/38] feat/2.0.0/wasm --- FloconAndroid/AGENT.md | 52 +++++++++++++++++ .../database/core-no-op/build.gradle.kts | 6 ++ FloconAndroid/database/core/build.gradle.kts | 6 ++ .../core/model/FloconDatabaseModel.wasmJs.kt | 13 +++++ .../database/room3-no-op/build.gradle.kts | 7 +++ FloconAndroid/database/room3/build.gradle.kts | 36 ++++++++---- .../room3/FloconRoom3DatabaseConfig.kt | 0 .../room3/FloconRoom3DatabaseModel.kt | 0 .../FloconRoom3DatabaseProviderImpl.wasmJs.kt | 20 +++++++ .../datastores-no-op/build.gradle.kts | 52 +++++++++++++++++ .../{main => androidMain}/AndroidManifest.xml | 0 .../model/FloconDatastorePreference.kt | 8 +-- FloconAndroid/datastores/build.gradle.kts | 58 ++++++++++++++++++- .../{main => androidMain}/AndroidManifest.xml | 0 .../model/FloconDatastorePreference.kt | 18 +++--- .../deeplinks-no-op/build.gradle.kts | 1 + FloconAndroid/deeplinks/build.gradle.kts | 6 ++ FloconAndroid/flocon-no-op/build.gradle.kts | 1 + .../github/openflocon/flocon/Flocon.wasmJs.kt | 8 +++ FloconAndroid/flocon/build.gradle.kts | 1 + .../flocon/utils/DispatcherUtils.android.kt | 8 +++ .../flocon/utils/DispatcherUtils.kt | 8 +++ .../flocon/utils/DispatcherUtils.ios.kt | 8 +++ .../flocon/utils/DispatcherUtils.jvm.kt | 8 +++ .../openflocon/flocon/FloconContext.wasmJs.kt | 3 + .../openflocon/flocon/FloconCore.wasmJs.kt | 4 ++ .../openflocon/flocon/FloconFile.wasmJs.kt | 6 ++ .../openflocon/flocon/FloconLogger.wasmJs.kt | 18 ++++++ .../openflocon/flocon/ServerHost.wasmJs.kt | 3 + .../openflocon/flocon/core/AppInfos.wasmJs.kt | 11 ++++ .../FloconCrashReporterDataSource.wasmJs.kt | 16 +++++ .../device/FloconDevicePluginImpl.wasmJs.kt | 6 ++ .../plugins/device/GetAppIconUtils.wasmJs.kt | 5 ++ .../plugins/files/FloconFilesPlugin.wasmJs.kt | 22 +++++++ .../FloconSharedPrefsPlugin.wasmJs.kt | 10 ++++ .../flocon/utils/DispatcherUtils.wasmJs.kt | 8 +++ .../flocon/utils/PlatformUtils.wasmJs.kt | 11 ++++ .../websocket/FloconHttpClient.wasmJs.kt | 18 ++++++ .../websocket/FloconWebSocketClient.wasmJs.kt | 19 ++++++ .../network/core-no-op/build.gradle.kts | 6 ++ FloconAndroid/network/core/build.gradle.kts | 6 ++ .../FloconNetworkDataSource.wasmJs.kt | 14 +++++ .../ktor-interceptor-no-op/build.gradle.kts | 6 ++ .../network/ktor-interceptor/build.gradle.kts | 6 ++ .../openflocon/flocon/ktor/Utils.wasmJs.kt | 6 ++ 45 files changed, 504 insertions(+), 25 deletions(-) create mode 100644 FloconAndroid/AGENT.md create mode 100644 FloconAndroid/database/core/src/wasmJsMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.wasmJs.kt rename FloconAndroid/database/room3/src/{commonMain => roomMain}/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt (100%) rename FloconAndroid/database/room3/src/{commonMain => roomMain}/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt (100%) create mode 100644 FloconAndroid/database/room3/src/wasmJsMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.wasmJs.kt rename FloconAndroid/datastores-no-op/src/{main => androidMain}/AndroidManifest.xml (100%) rename FloconAndroid/datastores-no-op/src/{main => commonMain}/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt (72%) rename FloconAndroid/datastores/src/{main => androidMain}/AndroidManifest.xml (100%) rename FloconAndroid/datastores/src/{main => commonMain}/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt (86%) create mode 100644 FloconAndroid/flocon-no-op/src/wasmJsMain/kotlin/io/github/openflocon/flocon/Flocon.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.android.kt create mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.kt create mode 100644 FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.ios.kt create mode 100644 FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.jvm.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconContext.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconCore.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconFile.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconLogger.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/ServerHost.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/core/AppInfos.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.wasmJs.kt create mode 100644 FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/websocket/FloconWebSocketClient.wasmJs.kt create mode 100644 FloconAndroid/network/core/src/wasmJsMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.wasmJs.kt create mode 100644 FloconAndroid/network/ktor-interceptor/src/wasmJsMain/kotlin/io/github/openflocon/flocon/ktor/Utils.wasmJs.kt diff --git a/FloconAndroid/AGENT.md b/FloconAndroid/AGENT.md new file mode 100644 index 000000000..de224d978 --- /dev/null +++ b/FloconAndroid/AGENT.md @@ -0,0 +1,52 @@ +# Flocon Project Overview + +Flocon is a modular, plugin-based framework built with **Kotlin Multiplatform (KMP)**. It provides a standardized way to integrate common cross-cutting concerns like networking, database access, datastores, and deep linking into applications. + +## 🚀 Key Features + +- **Modular Architecture**: Separate modules for different functionalities (network, database, etc.). +- **Plugin System**: Easily extensible with "no-op" variants for testing and modularity. +- **KMP Support**: Targets Android, iOS, JVM, and WasmJs. +- **Modern Tech Stack**: Uses Room 3, Ktor, OkHttp, gRPC, and Compose Multiplatform. + +## 🛠 Technical Stack + +- **Kotlin**: 2.1.0 +- **Build System**: Gradle with Version Catalog (`libs.versions.toml`). +- **Dependency Injection**: Manual / Constructor injection (based on current exploration). +- **Asynchronous Programming**: Kotlin Coroutines & Flow. +- **Networking**: Ktor 3.x, OkHttp 4.x, gRPC 1.70.x. +- **Database**: Room 2.x & Room 3.0.0-alpha01. +- **UI**: Compose Multiplatform 1.9.0. + +## 📂 Module Structure + +- `:flocon`: Core library providing the plugin registration and context management. +- `:database`: + - `:database:core`: Abstractions for database providers. + - `:database:room` / `:database:room3`: Room-based implementations. +- `:network`: + - `:network:core`: Core networking abstractions. + - `:network:okhttp-interceptor` / `:network:ktor-interceptor`: Client-specific interceptors. +- `:grpc`: + - `:grpc-interceptor`: Interceptors for gRPC calls. +- `:datastores`: Modules for Typed DataStore integration. +- `:deeplinks`: Abstractions and implementations for handling deep links. + +## 🧩 Core Concepts + +### Plugins +Flocon operates on a plugin-based architecture. Modules typically provide a "Core" or implementation module and a "No-Op" module. The No-Op modules allow the app to compile and run without the actual implementation, which is useful for specialized builds or testing. + +### Interceptors +For networking and gRPC, Flocon uses an interceptor-based approach to hook into the communication pipeline of standard libraries (OkHttp, Ktor, gRPC). + +## 📖 Development Guidelines + +- **Multiplatform First**: Always consider KMP compatibility when adding new features or modules. +- **Modularity**: Keep modules focused and avoid circular dependencies. +- **Naming Conventions**: Follow the `io.github.openflocon.flocon` package naming structure. +- **Version Catalog**: All dependency versions must be managed in `gradle/libs.versions.toml`. + +--- +*Created by Antigravity AI to assist in project understanding.* diff --git a/FloconAndroid/database/core-no-op/build.gradle.kts b/FloconAndroid/database/core-no-op/build.gradle.kts index 348ce3da1..ce8b7acb7 100644 --- a/FloconAndroid/database/core-no-op/build.gradle.kts +++ b/FloconAndroid/database/core-no-op/build.gradle.kts @@ -4,6 +4,11 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_database_core_no_op" + browser() + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -25,6 +30,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/database/core/build.gradle.kts b/FloconAndroid/database/core/build.gradle.kts index 346a4e2b9..5dffc225e 100644 --- a/FloconAndroid/database/core/build.gradle.kts +++ b/FloconAndroid/database/core/build.gradle.kts @@ -5,6 +5,11 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_database_core" + browser() + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -29,6 +34,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/database/core/src/wasmJsMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.wasmJs.kt b/FloconAndroid/database/core/src/wasmJsMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.wasmJs.kt new file mode 100644 index 000000000..4888f7e96 --- /dev/null +++ b/FloconAndroid/database/core/src/wasmJsMain/kotlin/io/github/openflocon/flocon/database/core/model/FloconDatabaseModel.wasmJs.kt @@ -0,0 +1,13 @@ +package io.github.openflocon.flocon.database.core.model + +import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecuteSqlResponse + +actual fun openDbAndExecuteQuery( + path: String, + query: String +): DatabaseExecuteSqlResponse { + return DatabaseExecuteSqlResponse.Error( + message = "SQLite is not supported on WasmJS yet", + originalSql = query + ) +} diff --git a/FloconAndroid/database/room3-no-op/build.gradle.kts b/FloconAndroid/database/room3-no-op/build.gradle.kts index 6507ac962..a251c1dcf 100644 --- a/FloconAndroid/database/room3-no-op/build.gradle.kts +++ b/FloconAndroid/database/room3-no-op/build.gradle.kts @@ -19,6 +19,12 @@ kotlin { iosArm64() iosSimulatorArm64() + wasmJs { + moduleName = "flocon_database_room3_no_op" + browser() + binaries.executable() + } + sourceSets { val commonMain by getting { dependencies { @@ -41,6 +47,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/database/room3/build.gradle.kts b/FloconAndroid/database/room3/build.gradle.kts index 3e7385b5f..3f94213c4 100644 --- a/FloconAndroid/database/room3/build.gradle.kts +++ b/FloconAndroid/database/room3/build.gradle.kts @@ -17,35 +17,50 @@ kotlin { jvm() + iosX64() iosArm64() iosSimulatorArm64() + wasmJs { + moduleName = "flocon_database_room3" + browser() + binaries.executable() + } + sourceSets { val commonMain by getting { dependencies { implementation(project(":flocon")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) implementation(project(":database:core")) + } + } + + val roomMain by creating { + dependsOn(commonMain) + dependencies { implementation(libs.androidx.room3.runtime) implementation(libs.androidx.sqlite.bundled) } } - + val androidMain by getting { - dependencies { - } + dependsOn(roomMain) } - + val jvmMain by getting { - dependencies { - } + dependsOn(roomMain) } - val iosArm64Main by getting - val iosSimulatorArm64Main by getting val iosMain by creating { + dependsOn(roomMain) + } + val iosX64Main by getting { dependsOn(iosMain) } + val iosArm64Main by getting { dependsOn(iosMain) } + val iosSimulatorArm64Main by getting { dependsOn(iosMain) } + + val wasmJsMain by getting { dependsOn(commonMain) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) } } } @@ -59,6 +74,7 @@ dependencies { add("kspCommonMainMetadata", libs.androidx.room3.compiler) add("kspAndroid", libs.androidx.room3.compiler) add("kspJvm", libs.androidx.room3.compiler) + add("kspIosX64", libs.androidx.room3.compiler) add("kspIosArm64", libs.androidx.room3.compiler) add("kspIosSimulatorArm64", libs.androidx.room3.compiler) } diff --git a/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt b/FloconAndroid/database/room3/src/roomMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt similarity index 100% rename from FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt rename to FloconAndroid/database/room3/src/roomMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseConfig.kt diff --git a/FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt b/FloconAndroid/database/room3/src/roomMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt similarity index 100% rename from FloconAndroid/database/room3/src/commonMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt rename to FloconAndroid/database/room3/src/roomMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseModel.kt diff --git a/FloconAndroid/database/room3/src/wasmJsMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.wasmJs.kt b/FloconAndroid/database/room3/src/wasmJsMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.wasmJs.kt new file mode 100644 index 000000000..ffadd900b --- /dev/null +++ b/FloconAndroid/database/room3/src/wasmJsMain/kotlin/io/github/openflocon/flocon/database/room3/FloconRoom3DatabaseProviderImpl.wasmJs.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.flocon.database.room3 + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.dsl.FloconMarker + +@OptIn(markerClass = [FloconMarker::class]) +internal actual class FloconRoom3DatabaseProviderImpl actual constructor( + private val context: FloconContext, + paths: List +) : FloconRoom3DatabaseProvider { + + actual override fun register() { + } + + @FloconMarker + actual override fun getAllDataBases(registeredDatabases: List): List { + return emptyList() + } +} diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index 5f4154d85..282d69731 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -5,6 +5,58 @@ plugins { id("flocon.publish") } +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + wasmJs { + moduleName = "flocon_datastores_no_op" + browser() + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + } + } + + val androidMain by getting { + dependencies { + } + } + + val jvmMain by getting { + dependencies { + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val wasmJsMain by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + android { namespace = "io.github.openflocon.flocon.datastores" } diff --git a/FloconAndroid/datastores-no-op/src/main/AndroidManifest.xml b/FloconAndroid/datastores-no-op/src/androidMain/AndroidManifest.xml similarity index 100% rename from FloconAndroid/datastores-no-op/src/main/AndroidManifest.xml rename to FloconAndroid/datastores-no-op/src/androidMain/AndroidManifest.xml diff --git a/FloconAndroid/datastores-no-op/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt b/FloconAndroid/datastores-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt similarity index 72% rename from FloconAndroid/datastores-no-op/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt rename to FloconAndroid/datastores-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt index cfe707327..52d23e6b7 100644 --- a/FloconAndroid/datastores-no-op/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt +++ b/FloconAndroid/datastores-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt @@ -1,9 +1,7 @@ package io.github.openflocon.flocon.preferences.datastores.model -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import io.github.openflocon.flocon.`plugins-old`.sharedprefs.model.FloconPreference -import io.github.openflocon.flocon.`plugins-old`.sharedprefs.model.FloconPreferenceValue +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreferenceValue interface FloconDatastoreMapper { fun fromDatastore(datastoreValue: String) : String @@ -12,7 +10,7 @@ interface FloconDatastoreMapper { class FloconDatastorePreference( override val name: String, - val dataStore: DataStore, + @Suppress("UNUSED_PARAMETER") dataStore: Any? = null, ) : io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference { override suspend fun set( diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 19c695d5f..3e2a6e0b3 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -5,11 +5,67 @@ plugins { id("flocon.publish") } +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + wasmJs { + moduleName = "flocon_datastores" + browser() + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.androidx.datastore.preferences) + } + } + + val jvmMain by getting { + dependencies { + implementation(libs.androidx.datastore.preferences) + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val wasmJsMain by getting + val iosMain by creating { + dependsOn(commonMain) + dependencies { + implementation(libs.androidx.datastore.preferences) + } + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + android { namespace = "io.github.openflocon.flocon.datastores" } -dependencies { implementation(projects.flocon) diff --git a/FloconAndroid/datastores/src/main/AndroidManifest.xml b/FloconAndroid/datastores/src/androidMain/AndroidManifest.xml similarity index 100% rename from FloconAndroid/datastores/src/main/AndroidManifest.xml rename to FloconAndroid/datastores/src/androidMain/AndroidManifest.xml diff --git a/FloconAndroid/datastores/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt b/FloconAndroid/datastores/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt similarity index 86% rename from FloconAndroid/datastores/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt rename to FloconAndroid/datastores/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt index 4e71bce4c..12f2ce12e 100644 --- a/FloconAndroid/datastores/src/main/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt +++ b/FloconAndroid/datastores/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt @@ -1,14 +1,14 @@ package io.github.openflocon.flocon.preferences.datastores.model -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.doublePreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.floatPreferencesKey -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey +//import androidx.datastore.core.DataStore +//import androidx.datastore.preferences.core.Preferences +//import androidx.datastore.preferences.core.booleanPreferencesKey +//import androidx.datastore.preferences.core.doublePreferencesKey +//import androidx.datastore.preferences.core.edit +//import androidx.datastore.preferences.core.floatPreferencesKey +//import androidx.datastore.preferences.core.intPreferencesKey +//import androidx.datastore.preferences.core.longPreferencesKey +//import androidx.datastore.preferences.core.stringPreferencesKey import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreference import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconPreferenceValue diff --git a/FloconAndroid/deeplinks-no-op/build.gradle.kts b/FloconAndroid/deeplinks-no-op/build.gradle.kts index 438e43cdb..f51058fa5 100644 --- a/FloconAndroid/deeplinks-no-op/build.gradle.kts +++ b/FloconAndroid/deeplinks-no-op/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/deeplinks/build.gradle.kts b/FloconAndroid/deeplinks/build.gradle.kts index 0049a7aef..352c80faf 100644 --- a/FloconAndroid/deeplinks/build.gradle.kts +++ b/FloconAndroid/deeplinks/build.gradle.kts @@ -5,6 +5,11 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_deeplinks" + binaries.executable() + browser() + } sourceSets { val commonMain by getting { dependencies { @@ -27,6 +32,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index 99ab4b8c7..03b4aac00 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/flocon-no-op/src/wasmJsMain/kotlin/io/github/openflocon/flocon/Flocon.wasmJs.kt b/FloconAndroid/flocon-no-op/src/wasmJsMain/kotlin/io/github/openflocon/flocon/Flocon.wasmJs.kt new file mode 100644 index 000000000..88207316f --- /dev/null +++ b/FloconAndroid/flocon-no-op/src/wasmJsMain/kotlin/io/github/openflocon/flocon/Flocon.wasmJs.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocon + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +actual object Flocon : FloconApp { + override val isInitialized: StateFlow = MutableStateFlow(false) +} diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index a60a1be31..a89f9dbd0 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -38,6 +38,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.android.kt new file mode 100644 index 000000000..382fa2232 --- /dev/null +++ b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.android.kt @@ -0,0 +1,8 @@ +package kotlinx.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +public actual val Dispatchers.IO: CoroutineDispatcher get() = Dispatchers.IO + +public actual val IO: CoroutineDispatcher get() = Dispatchers.IO diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.kt new file mode 100644 index 000000000..be94d1ed8 --- /dev/null +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.kt @@ -0,0 +1,8 @@ +package kotlinx.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +public expect val Dispatchers.IO: CoroutineDispatcher + +public expect val IO: CoroutineDispatcher diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.ios.kt new file mode 100644 index 000000000..7c711bc0a --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.ios.kt @@ -0,0 +1,8 @@ +package kotlinx.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +public actual val Dispatchers.IO: CoroutineDispatcher get() = Dispatchers.Default + +public actual val IO: CoroutineDispatcher get() = Dispatchers.Default diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.jvm.kt new file mode 100644 index 000000000..382fa2232 --- /dev/null +++ b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.jvm.kt @@ -0,0 +1,8 @@ +package kotlinx.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +public actual val Dispatchers.IO: CoroutineDispatcher get() = Dispatchers.IO + +public actual val IO: CoroutineDispatcher get() = Dispatchers.IO diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconContext.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconContext.wasmJs.kt new file mode 100644 index 000000000..c6158c9c2 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconContext.wasmJs.kt @@ -0,0 +1,3 @@ +package io.github.openflocon.flocon + +actual class FloconContext diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconCore.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconCore.wasmJs.kt new file mode 100644 index 000000000..22cbf2293 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconCore.wasmJs.kt @@ -0,0 +1,4 @@ +package io.github.openflocon.flocon + +internal actual fun displayClearTextError(context: FloconContext) { +} diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconFile.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconFile.wasmJs.kt new file mode 100644 index 000000000..5b4a91e15 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconFile.wasmJs.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon + +import io.github.openflocon.flocon.dsl.FloconMarker + +@FloconMarker +actual class FloconFile diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconLogger.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconLogger.wasmJs.kt new file mode 100644 index 000000000..637d7a7c9 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/FloconLogger.wasmJs.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocon + +actual object FloconLogger { + actual var enabled = false + + actual fun logError(text: String, throwable: Throwable?) { + if(enabled) { + println("ERROR: $text") + throwable?.printStackTrace() + } + } + + actual fun log(text: String) { + if(enabled) { + println("DEBUG: $text") + } + } +} diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/ServerHost.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/ServerHost.wasmJs.kt new file mode 100644 index 000000000..489dcbce0 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/ServerHost.wasmJs.kt @@ -0,0 +1,3 @@ +package io.github.openflocon.flocon + +internal actual fun getServerHost(floconContext: FloconContext): String = "localhost" diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/core/AppInfos.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/core/AppInfos.wasmJs.kt new file mode 100644 index 000000000..e971bc432 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/core/AppInfos.wasmJs.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.core + +import io.github.openflocon.flocon.FloconContext + +internal actual fun getAppInfos(floconContext: FloconContext): AppInfos = AppInfos( + deviceId = "wasm-device", + deviceName = "Browser", + appName = "Flocon Wasm", + appPackageName = "io.github.openflocon.wasm", + platform = "wasm" +) diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.wasmJs.kt new file mode 100644 index 000000000..c35073b68 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.wasmJs.kt @@ -0,0 +1,16 @@ +package io.github.openflocon.flocon.plugins.crashreporter + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel + +internal actual fun buildFloconCrashReporterDataSource(context: FloconContext): FloconCrashReporterDataSource = object : FloconCrashReporterDataSource { + override fun saveCrash(crash: CrashReportDataModel) {} + override fun loadPendingCrashes(): List = emptyList() + override fun deleteCrash(crashId: String) {} +} + +internal actual fun setupUncaughtExceptionHandler( + context: FloconContext, + onCrash: (Throwable) -> Unit +) { +} diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt new file mode 100644 index 000000000..93191f721 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.plugins.device + +import io.github.openflocon.flocon.FloconContext + +internal actual fun restartApp(context: FloconContext) { +} diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt new file mode 100644 index 000000000..cb477e7a2 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt @@ -0,0 +1,5 @@ +package io.github.openflocon.flocon.plugins.device + +import io.github.openflocon.flocon.FloconContext + +actual fun getAppIconBase64(context: FloconContext): String? = null diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt new file mode 100644 index 000000000..41a60203b --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.flocon.plugins.files + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.plugins.files.model.fromdevice.FileDataModel + +internal actual fun fileDataSource(context: FloconContext): FileDataSource = FloconFilesDataSourceWasmJs() + +private class FloconFilesDataSourceWasmJs : FileDataSource { + @FloconMarker + override fun getFolderContent(path: String, isConstantPath: Boolean, withFoldersSize: Boolean): List = emptyList() + + @FloconMarker + override fun getFile(path: String, isConstantPath: Boolean): FloconFile? = null + + override fun deleteFile(path: String) {} + override fun deleteFiles(path: List) {} + + @FloconMarker + override fun deleteFolderContent(folder: FloconFile) {} +} diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt new file mode 100644 index 000000000..3dc844cfd --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt @@ -0,0 +1,10 @@ +package io.github.openflocon.flocon.plugins.sharedprefs + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconSharedPreferenceModel + +internal actual fun buildFloconSharedPreferenceDataSource(context: FloconContext): FloconSharedPreferenceDataSource = object : FloconSharedPreferenceDataSource { + override fun getSharedPreferences(): List = emptyList() + override fun getSharedPreferenceValue(fileName: String, key: String): String? = null + override fun setSharedPreferenceValue(fileName: String, key: String, value: String) {} +} diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.wasmJs.kt new file mode 100644 index 000000000..7c711bc0a --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/utils/DispatcherUtils.wasmJs.kt @@ -0,0 +1,8 @@ +package kotlinx.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +public actual val Dispatchers.IO: CoroutineDispatcher get() = Dispatchers.Default + +public actual val IO: CoroutineDispatcher get() = Dispatchers.Default diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.wasmJs.kt new file mode 100644 index 000000000..ec9cb0ebe --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/utils/PlatformUtils.wasmJs.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocon.utils + +// Using a simple polyfill/wrapper for time in wasmJs if needed, +// but for now we'll try to use what's available or stubs to make it compile. +// Note: In a real app, you'd use a library like kotlinx-datetime. + +actual fun currentTimeMillis(): Long = 0L // Stub for now to ensure compilation + +actual fun currentTimeNanos(): Long = 0L // Stub for now to ensure compilation + +actual fun createThrowableFromClassName(className: String): Throwable? = null diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.wasmJs.kt new file mode 100644 index 000000000..c5bb72acf --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.wasmJs.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocon.websocket + +import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.dsl.FloconMarker +import io.github.openflocon.flocon.model.FloconFileInfo + +internal actual fun buildFloconHttpClient(): FloconHttpClient = object : FloconHttpClient { + @FloconMarker + override suspend fun send( + file: FloconFile, + infos: FloconFileInfo, + address: String, + port: Int, + deviceId: String, + appPackageName: String, + appInstance: Long + ): Boolean = false +} diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/websocket/FloconWebSocketClient.wasmJs.kt b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/websocket/FloconWebSocketClient.wasmJs.kt new file mode 100644 index 000000000..ca5e53a22 --- /dev/null +++ b/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/websocket/FloconWebSocketClient.wasmJs.kt @@ -0,0 +1,19 @@ +package io.github.openflocon.flocon.websocket + +internal actual fun buildFloconWebSocketClient(): FloconWebSocketClient = object : FloconWebSocketClient { + override suspend fun connect( + address: String, + port: Int, + onMessageReceived: (String) -> Unit, + onClosed: () -> Unit + ) { + } + + override suspend fun sendPendingMessages() { + } + + override suspend fun sendMessage(message: String): Boolean = false + + override suspend fun disconnect() { + } +} diff --git a/FloconAndroid/network/core-no-op/build.gradle.kts b/FloconAndroid/network/core-no-op/build.gradle.kts index 27fa6b6cf..7247ac2a8 100644 --- a/FloconAndroid/network/core-no-op/build.gradle.kts +++ b/FloconAndroid/network/core-no-op/build.gradle.kts @@ -4,6 +4,11 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_network_core_no_op" + browser() + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -25,6 +30,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/network/core/build.gradle.kts b/FloconAndroid/network/core/build.gradle.kts index a677713ae..22cfe5c78 100644 --- a/FloconAndroid/network/core/build.gradle.kts +++ b/FloconAndroid/network/core/build.gradle.kts @@ -5,6 +5,11 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_network_core" + browser() + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -27,6 +32,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/network/core/src/wasmJsMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.wasmJs.kt b/FloconAndroid/network/core/src/wasmJsMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.wasmJs.kt new file mode 100644 index 000000000..271cc6a48 --- /dev/null +++ b/FloconAndroid/network/core/src/wasmJsMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.wasmJs.kt @@ -0,0 +1,14 @@ +package io.github.openflocon.flocon.network.core.datasource + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.network.core.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.MockNetworkResponse + +internal actual inline fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource = FloconNetworkDataSourceWasmJs() + +internal class FloconNetworkDataSourceWasmJs : FloconNetworkDataSource { + override fun saveMocksToFile(mocks: List) {} + override fun loadMocksFromFile(): List = emptyList() + override fun saveBadNetworkConfig(config: BadQualityConfig?) {} + override fun loadBadNetworkConfig(): BadQualityConfig? = null +} diff --git a/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts index 821e8a0a5..ba1789fd4 100644 --- a/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts @@ -4,6 +4,11 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_ktor_interceptor_no_op" + browser() + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -25,6 +30,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/network/ktor-interceptor/build.gradle.kts b/FloconAndroid/network/ktor-interceptor/build.gradle.kts index 57fd88634..4bca9d162 100644 --- a/FloconAndroid/network/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor/build.gradle.kts @@ -4,6 +4,11 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_ktor_interceptor" + browser() + } + binaries.executable() sourceSets { val commonMain by getting { dependencies { @@ -31,6 +36,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting + val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/network/ktor-interceptor/src/wasmJsMain/kotlin/io/github/openflocon/flocon/ktor/Utils.wasmJs.kt b/FloconAndroid/network/ktor-interceptor/src/wasmJsMain/kotlin/io/github/openflocon/flocon/ktor/Utils.wasmJs.kt new file mode 100644 index 000000000..d4511a451 --- /dev/null +++ b/FloconAndroid/network/ktor-interceptor/src/wasmJsMain/kotlin/io/github/openflocon/flocon/ktor/Utils.wasmJs.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.flocon.ktor + +internal actual fun decodeNetworkBody( + bytes: ByteArray, + headers: Map +): String = bytes.decodeToString() From 3f82b4e95bbcb1b59d78f6fab36b85e3e78d6332 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 8 Apr 2026 14:33:24 +0200 Subject: [PATCH 26/38] feat: Gradle --- FloconAndroid/database/room3/build.gradle.kts | 29 ++----------------- FloconAndroid/gradle/libs.versions.toml | 2 ++ 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/FloconAndroid/database/room3/build.gradle.kts b/FloconAndroid/database/room3/build.gradle.kts index 3f94213c4..5e26920c7 100644 --- a/FloconAndroid/database/room3/build.gradle.kts +++ b/FloconAndroid/database/room3/build.gradle.kts @@ -17,7 +17,6 @@ kotlin { jvm() - iosX64() iosArm64() iosSimulatorArm64() @@ -31,34 +30,13 @@ kotlin { val commonMain by getting { dependencies { implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) - implementation(project(":database:core")) - } - } - - val roomMain by creating { - dependsOn(commonMain) - dependencies { + implementation(libs.kotlinx.coroutines.core) implementation(libs.androidx.room3.runtime) - implementation(libs.androidx.sqlite.bundled) + implementation(libs.androidx.sqlite) + implementation(project(":database:core")) } } - val androidMain by getting { - dependsOn(roomMain) - } - - val jvmMain by getting { - dependsOn(roomMain) - } - - val iosMain by creating { - dependsOn(roomMain) - } - val iosX64Main by getting { dependsOn(iosMain) } - val iosArm64Main by getting { dependsOn(iosMain) } - val iosSimulatorArm64Main by getting { dependsOn(iosMain) } - val wasmJsMain by getting { dependsOn(commonMain) } @@ -74,7 +52,6 @@ dependencies { add("kspCommonMainMetadata", libs.androidx.room3.compiler) add("kspAndroid", libs.androidx.room3.compiler) add("kspJvm", libs.androidx.room3.compiler) - add("kspIosX64", libs.androidx.room3.compiler) add("kspIosArm64", libs.androidx.room3.compiler) add("kspIosSimulatorArm64", libs.androidx.room3.compiler) } diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index 2fba6fb4b..c3a757f69 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -29,6 +29,7 @@ protobuf = "4.26.1" ksp = "2.1.21-2.0.2" processPhoenix = "3.0.0" sqlite = "2.6.2" +sqlite-alpha = "2.7.0-alpha02" sqliteJdbc = "3.50.3.0" buildconfig = "5.6.8" brotli = "0.1.2" @@ -93,6 +94,7 @@ protobuf-util = { group = "com.google.protobuf", name = "protobuf-java-util", ve sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" } +androidx-sqlite = { module = "androidx.sqlite:sqlite", version.ref = "sqlite-alpha" } androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } From 3078a2a5f04d9cce8975e30fbd8aeb4a1182ba99 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 13 May 2026 14:26:55 +0200 Subject: [PATCH 27/38] fix: Gradle --- .../datastores-no-op/build.gradle.kts | 48 +++++++------- FloconAndroid/datastores/build.gradle.kts | 65 ++++++++----------- .../deeplinks-no-op/build.gradle.kts | 2 +- FloconAndroid/flocon-no-op/build.gradle.kts | 2 +- FloconAndroid/flocon/build.gradle.kts | 2 +- .../network/ktor-interceptor/build.gradle.kts | 5 +- 6 files changed, 56 insertions(+), 68 deletions(-) diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index 282d69731..16019d4ef 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -1,36 +1,36 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - id("flocon.android.library") + id("flocon.kotlin.multiplatform") id("flocon.publish") } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - - wasmJs { - moduleName = "flocon_datastores_no_op" - browser() - binaries.executable() - } +// androidTarget { +// compilations.all { +// kotlinOptions { +// jvmTarget = "11" +// } +// } +// } +// +// jvm() +// +// iosX64() +// iosArm64() +// iosSimulatorArm64() +// +// wasmJs { +// moduleName = "flocon_datastores_no_op" +// browser() +// binaries.executable() +// } sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(projects.flocon) + implementation(libs.kotlinx.coroutines.core) } } @@ -47,7 +47,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting +// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) @@ -79,4 +79,4 @@ mavenPublishing { artifactId = "flocon-datastores-no-op", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) -} \ No newline at end of file +} diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 3e2a6e0b3..145e55c53 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -1,45 +1,45 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.internal.platform.wasm.WasmPlatforms.wasmJs plugins { - id("flocon.android.library") + id("flocon.kotlin.multiplatform") id("flocon.publish") } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - - wasmJs { - moduleName = "flocon_datastores" - browser() - binaries.executable() - } +// androidTarget { +// compilations.all { +// kotlinOptions { +// jvmTarget = "11" +// } +// } +// } +// +// jvm() +// +// iosX64() +// iosArm64() +// iosSimulatorArm64() +// +// wasmJs { +// moduleName = "flocon_datastores" +// browser() +// binaries.executable() +// } sourceSets { val commonMain by getting { dependencies { - implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(projects.flocon) + implementation(libs.kotlinx.coroutines.core) } } - + val androidMain by getting { dependencies { implementation(libs.androidx.datastore.preferences) } } - + val jvmMain by getting { dependencies { implementation(libs.androidx.datastore.preferences) @@ -49,7 +49,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting +// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) dependencies { @@ -66,21 +66,10 @@ android { namespace = "io.github.openflocon.flocon.datastores" } - - implementation(projects.flocon) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.android) - - implementation(libs.androidx.datastore.preferences) -} - - mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-datastores", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) -} \ No newline at end of file +} diff --git a/FloconAndroid/deeplinks-no-op/build.gradle.kts b/FloconAndroid/deeplinks-no-op/build.gradle.kts index f51058fa5..b87ee9109 100644 --- a/FloconAndroid/deeplinks-no-op/build.gradle.kts +++ b/FloconAndroid/deeplinks-no-op/build.gradle.kts @@ -25,7 +25,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting +// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index 03b4aac00..8ea9ff130 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -27,7 +27,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting +// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index a89f9dbd0..907726d84 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -38,7 +38,7 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting +// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/network/ktor-interceptor/build.gradle.kts b/FloconAndroid/network/ktor-interceptor/build.gradle.kts index 4bca9d162..2c44a9a8c 100644 --- a/FloconAndroid/network/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor/build.gradle.kts @@ -7,8 +7,9 @@ kotlin { wasmJs { moduleName = "flocon_ktor_interceptor" browser() - } binaries.executable() + } + sourceSets { val commonMain by getting { dependencies { @@ -50,8 +51,6 @@ android { namespace = "io.github.openflocon.flocon.ktor" } - - mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, From 85f9cbedcdde32c2a116ac97573bd7a1b0863bcf Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 13 May 2026 14:29:51 +0200 Subject: [PATCH 28/38] feat: Wasm --- .../database/core-no-op/build.gradle.kts | 12 +----- FloconAndroid/database/core/build.gradle.kts | 13 +------ .../datastores-no-op/build.gradle.kts | 37 +++--------------- FloconAndroid/datastores/build.gradle.kts | 39 +++---------------- .../deeplinks-no-op/build.gradle.kts | 17 +++----- FloconAndroid/deeplinks/build.gradle.kts | 12 +----- FloconAndroid/flocon-no-op/build.gradle.kts | 17 +++----- FloconAndroid/flocon/build.gradle.kts | 9 +++-- 8 files changed, 31 insertions(+), 125 deletions(-) diff --git a/FloconAndroid/database/core-no-op/build.gradle.kts b/FloconAndroid/database/core-no-op/build.gradle.kts index ce8b7acb7..69d424509 100644 --- a/FloconAndroid/database/core-no-op/build.gradle.kts +++ b/FloconAndroid/database/core-no-op/build.gradle.kts @@ -9,6 +9,7 @@ kotlin { browser() binaries.executable() } + sourceSets { val commonMain by getting { dependencies { @@ -16,21 +17,10 @@ kotlin { implementation(libs.kotlinx.coroutines.core) } } - - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - } - } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/database/core/build.gradle.kts b/FloconAndroid/database/core/build.gradle.kts index 5dffc225e..37da966f6 100644 --- a/FloconAndroid/database/core/build.gradle.kts +++ b/FloconAndroid/database/core/build.gradle.kts @@ -10,6 +10,7 @@ kotlin { browser() binaries.executable() } + sourceSets { val commonMain by getting { dependencies { @@ -19,22 +20,10 @@ kotlin { implementation(libs.kotlinx.serialization.json) } } - - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.sqlite.jdbc) - } - } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index 16019d4ef..b27fdd579 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -1,30 +1,14 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { id("flocon.kotlin.multiplatform") id("flocon.publish") } kotlin { -// androidTarget { -// compilations.all { -// kotlinOptions { -// jvmTarget = "11" -// } -// } -// } -// -// jvm() -// -// iosX64() -// iosArm64() -// iosSimulatorArm64() -// -// wasmJs { -// moduleName = "flocon_datastores_no_op" -// browser() -// binaries.executable() -// } + wasmJs { + moduleName = "flocon_datastores_no_op" + browser() + binaries.executable() + } sourceSets { val commonMain by getting { @@ -33,21 +17,10 @@ kotlin { implementation(libs.kotlinx.coroutines.core) } } - - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - } - } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting -// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 145e55c53..3fa34912b 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -1,30 +1,14 @@ -import org.jetbrains.kotlin.gradle.internal.platform.wasm.WasmPlatforms.wasmJs - plugins { id("flocon.kotlin.multiplatform") id("flocon.publish") } kotlin { -// androidTarget { -// compilations.all { -// kotlinOptions { -// jvmTarget = "11" -// } -// } -// } -// -// jvm() -// -// iosX64() -// iosArm64() -// iosSimulatorArm64() -// -// wasmJs { -// moduleName = "flocon_datastores" -// browser() -// binaries.executable() -// } + wasmJs { + moduleName = "flocon_datastores" + browser() + binaries.executable() + } sourceSets { val commonMain by getting { @@ -34,22 +18,9 @@ kotlin { } } - val androidMain by getting { - dependencies { - implementation(libs.androidx.datastore.preferences) - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.androidx.datastore.preferences) - } - } - val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting -// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) dependencies { diff --git a/FloconAndroid/deeplinks-no-op/build.gradle.kts b/FloconAndroid/deeplinks-no-op/build.gradle.kts index b87ee9109..4ab3d4ec4 100644 --- a/FloconAndroid/deeplinks-no-op/build.gradle.kts +++ b/FloconAndroid/deeplinks-no-op/build.gradle.kts @@ -4,6 +4,12 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_deeplinks_no_op" + binaries.executable() + browser() + } + sourceSets { val commonMain by getting { dependencies { @@ -12,20 +18,9 @@ kotlin { } } - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - } - } - val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting -// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/deeplinks/build.gradle.kts b/FloconAndroid/deeplinks/build.gradle.kts index 352c80faf..b29a68d10 100644 --- a/FloconAndroid/deeplinks/build.gradle.kts +++ b/FloconAndroid/deeplinks/build.gradle.kts @@ -10,6 +10,7 @@ kotlin { binaries.executable() browser() } + sourceSets { val commonMain by getting { dependencies { @@ -18,21 +19,10 @@ kotlin { implementation(libs.kotlinx.serialization.json) } } - - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - } - } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index 8ea9ff130..adbf62120 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -6,6 +6,12 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon_no_op" + binaries.executable() + browser() + } + sourceSets { val commonMain by getting { dependencies { @@ -13,21 +19,10 @@ kotlin { implementation(libs.kotlinx.coroutines.core) } } - - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - } - } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting -// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index 907726d84..960db4acb 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { id("flocon.kotlin.multiplatform") alias(libs.plugins.kotlin.serialization) @@ -8,6 +6,12 @@ plugins { } kotlin { + wasmJs { + moduleName = "flocon" + binaries.executable() + browser() + } + sourceSets { val commonMain by getting { dependencies { @@ -38,7 +42,6 @@ kotlin { val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting -// val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) From 9dd4c30a16ea20f7b0cad73a5308fe7c96a8183a Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 13 May 2026 14:32:42 +0200 Subject: [PATCH 29/38] fix: Clean --- FloconAndroid/flocon-no-op/build.gradle.kts | 2 - .../analytics/FloconAnalyticsPlugin.kt | 44 ------------------- .../analytics/builder/AnalyticsBuilder.kt | 32 -------------- .../analytics/model/AnalyticsEvent.kt | 11 ----- .../model/AnalyticsPropertiesConfig.kt | 11 ----- .../pluginsold/analytics/model/TableItem.kt | 9 ---- .../database/FloconDatabasePlugin.kt | 1 - .../database/model/FloconDatabaseModel.kt | 1 - .../ktor-interceptor-no-op/build.gradle.kts | 14 +----- 9 files changed, 1 insertion(+), 124 deletions(-) delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index adbf62120..445d245c4 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { id("flocon.kotlin.multiplatform") id("flocon.publish") diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt deleted file mode 100644 index e8bd6f068..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/FloconAnalyticsPlugin.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.analytics - -import io.github.openflocon.flocon.FloconConfig -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.FloconPluginFactory -import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem - -class FloconAnalyticsConfig : FloconPluginConfig - -/** - * Flocon Analytics Plugin. - */ -object FloconAnalytics : FloconPluginFactory { - override fun createConfig(context: FloconContext) = TODO() - override fun install( - pluginConfig: FloconAnalyticsConfig, - floconConfig: FloconConfig, - encoder: FloconEncoder - ): FloconAnalyticsPlugin = TODO() - - override val name: String = "" - override val pluginId: String = "ANALYTICS" -} -// -//fun floconAnalytics(analyticsName: String) : AnalyticsBuilder { -// return AnalyticsBuilder( -// analyticsTableId = analyticsName, -// analyticsPlugin = FloconApp.instance?.client?.analyticsPlugin, -// ) -//} - -//fun FloconApp.analytics(analyticsName: String): AnalyticsBuilder { -// return AnalyticsBuilder( -// analyticsTableId = analyticsName, -// analyticsPlugin = this.client?.analyticsPlugin, -// ) -//} - -interface FloconAnalyticsPlugin : FloconPlugin { - fun registerAnalytics(analyticsItems: List) -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt deleted file mode 100644 index f59c51620..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/builder/AnalyticsBuilder.kt +++ /dev/null @@ -1,32 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package io.github.openflocon.flocon.pluginsold.analytics.builder - -import io.github.openflocon.flocon.pluginsold.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsEvent -import io.github.openflocon.flocon.pluginsold.analytics.model.AnalyticsItem -import io.github.openflocon.flocon.utils.currentTimeMillis -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid - -class AnalyticsBuilder( - val analyticsTableId: String, - private val analyticsPlugin: FloconAnalyticsPlugin?, -) { - fun logEvents(vararg events: AnalyticsEvent) { - this.logEvents(events.toList()) - } - - fun logEvents(events: List) { - val analyticsItems = events.map { - AnalyticsItem( - id = Uuid.random().toString(), - analyticsTableId = analyticsTableId, - eventName = it.eventName, - createdAt = currentTimeMillis(), - properties = it.properties, - ) - } - analyticsPlugin?.registerAnalytics(analyticsItems) - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt deleted file mode 100644 index 10a608249..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.analytics.model - -data class AnalyticsEvent( - val eventName: String, - val properties: List, -) { - constructor( - eventName: String, - vararg properties: AnalyticsPropertiesConfig, - ) : this(eventName, properties.toList()) -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt deleted file mode 100644 index de6d93092..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/AnalyticsPropertiesConfig.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.analytics.model - -data class AnalyticsPropertiesConfig( - val name: String, - val value: String, -) - -infix fun String.analyticsProperty(value: String) = AnalyticsPropertiesConfig( - name = this, - value = value, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt deleted file mode 100644 index c23f13409..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/analytics/model/TableItem.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.analytics.model - -data class AnalyticsItem( - val id: String, - val analyticsTableId: String, - val eventName: String, - val createdAt: Long, - val properties: List, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt deleted file mode 100644 index 1d9511610..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/FloconDatabasePlugin.kt +++ /dev/null @@ -1 +0,0 @@ -// DEPRECATED: Moved to :database:core \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt deleted file mode 100644 index 0447420ba..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/database/model/FloconDatabaseModel.kt +++ /dev/null @@ -1 +0,0 @@ -// DEPRECATED \ No newline at end of file diff --git a/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts index ba1789fd4..852463ec0 100644 --- a/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/network/ktor-interceptor-no-op/build.gradle.kts @@ -9,6 +9,7 @@ kotlin { browser() binaries.executable() } + sourceSets { val commonMain by getting { dependencies { @@ -16,21 +17,10 @@ kotlin { implementation(libs.ktor.client.core) } } - - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - } - } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting - val wasmJsMain by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) @@ -44,8 +34,6 @@ android { namespace = "io.github.openflocon.flocon.ktor" } - - mavenPublishing { coordinates( groupId = project.property("floconGroupId") as String, From dd736566d483624af2dd82fae8abbcc78b223d7d Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 13 May 2026 14:48:39 +0200 Subject: [PATCH 30/38] feat: Cleaning --- .../analytics-no-op/build.gradle.kts | 2 +- FloconAndroid/analytics/build.gradle.kts | 78 +------------------ .../analytics/FloconAnalyticsPlugin.kt | 19 +++-- .../analytics/mapper/AnalyticsItemsMapper.kt | 12 +-- .../openflocon/buildlogic/AndroidConfig.kt | 22 ++++++ .../FloconAndroidLibraryConventionPlugin.kt | 26 +++++++ ...oconKotlinMultiplatformConventionPlugin.kt | 38 +++++++++ .../FloconPublishConventionPlugin.kt | 52 +++++++++++++ .../analytics/FloconAnalyticsPlugin.kt | 73 ----------------- .../analytics/builder/AnalyticsBuilder.kt | 32 -------- .../analytics/mapper/AnalyticsItemsMapper.kt | 34 -------- .../plugins/analytics/model/AnalyticsEvent.kt | 11 --- .../plugins/analytics/model/AnalyticsItem.kt | 9 --- .../model/AnalyticsPropertiesConfig.kt | 11 --- .../pluginsold/network/FloconNetworkPlugin.kt | 29 ------- .../network/model/BadQualityConfig.kt | 77 ------------------ .../network/model/FloconHttpRequest.kt | 23 ------ .../network/model/FloconNetworkCallRequest.kt | 8 -- .../model/FloconNetworkCallResponse.kt | 9 --- .../network/model/FloconWebSocketEvent.kt | 21 ----- .../model/FloconWebSocketMockListener.kt | 5 -- .../network/model/MockNetworkResponse.kt | 39 ---------- .../grpc-interceptor-base/build.gradle.kts | 2 +- .../openflocon/flocon/grpc/BadQuality.kt | 2 +- .../flocon/grpc/FloconGrpcBaseInterceptor.kt | 6 +- .../flocon/grpc/FloconGrpcPlugin.kt | 4 +- .../flocon/grpc/model/RequestHolder.kt | 2 +- .../sample-android-only/build.gradle.kts | 3 + FloconAndroid/settings.gradle.kts | 2 + 29 files changed, 168 insertions(+), 483 deletions(-) create mode 100644 FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/AndroidConfig.kt create mode 100644 FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconAndroidLibraryConventionPlugin.kt create mode 100644 FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconKotlinMultiplatformConventionPlugin.kt create mode 100644 FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconPublishConventionPlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/BadQualityConfig.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconHttpRequest.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallRequest.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallResponse.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketEvent.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketMockListener.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/MockNetworkResponse.kt diff --git a/FloconAndroid/analytics-no-op/build.gradle.kts b/FloconAndroid/analytics-no-op/build.gradle.kts index a7e3f4f02..1d040d352 100644 --- a/FloconAndroid/analytics-no-op/build.gradle.kts +++ b/FloconAndroid/analytics-no-op/build.gradle.kts @@ -23,7 +23,7 @@ kotlin { val commonMain by getting { dependencies { implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(libs.kotlinx.coroutines.core) } } } diff --git a/FloconAndroid/analytics/build.gradle.kts b/FloconAndroid/analytics/build.gradle.kts index 870ee0a77..dbdb7375e 100644 --- a/FloconAndroid/analytics/build.gradle.kts +++ b/FloconAndroid/analytics/build.gradle.kts @@ -1,30 +1,14 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.vanniktech.maven.publish) - alias(libs.plugins.kotlin.serialization) + id("flocon.kotlin.multiplatform") + id("flocon.publish") } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } - - jvm() - - iosX64() - iosArm64() - iosSimulatorArm64() - sourceSets { val commonMain by getting { dependencies { implementation(project(":flocon")) - implementation(libs.jetbrains.kotlinx.coroutines.core.fixed) + implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) } } @@ -33,68 +17,12 @@ kotlin { android { namespace = "io.github.openflocon.flocon.analytics" - compileSdk = 36 - - defaultConfig { - minSdk = 23 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } mavenPublishing { - publishToMavenCentral(automaticRelease = true) - - if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { - // Skip signing - } else { - signAllPublications() - } - coordinates( groupId = project.property("floconGroupId") as String, artifactId = "flocon-analytics", version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String ) - - pom { - name = "Flocon Analytics" - description = project.property("floconDescription") as String - inceptionYear = "2025" - url = "https://github.com/openflocon/Flocon" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - id = "openflocon" - name = "Open Flocon" - url = "https://github.com/openflocon" - } - } - scm { - url = "https://github.com/openflocon/Flocon" - connection = "scm:git:git://github.com/openflocon/Flocon.git" - developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" - } - } } diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt index 9e2fe0f5c..9af8d73c8 100644 --- a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt @@ -7,7 +7,9 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender +import io.github.openflocon.flocon.core.encode import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem class FloconAnalyticsConfig : FloconPluginConfig @@ -22,16 +24,19 @@ object FloconAnalytics : FloconPluginFactory) { analyticsItems.takeIf { it.isNotEmpty() }?.forEach { toSend -> try { -// sender.send( -// plugin = Protocol.FromDevice.Analytics.Plugin, -// method = Protocol.FromDevice.Analytics.Method.AddItems, -// body = analyticsItemsToJson(toSend) -// ) + sender.send( + plugin = Protocol.FromDevice.Analytics.Plugin, + method = Protocol.FromDevice.Analytics.Method.AddItems, + body = encoder.encode(toSend) + ) } catch (t: Throwable) { FloconLogger.logError("error on sendAnalytics", t) } diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt index e8ec738ee..fc835947f 100644 --- a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt @@ -1,17 +1,7 @@ package io.github.openflocon.flocon.plugins.analytics.mapper -import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString - -internal fun analyticsItemsToJson(item: AnalyticsItem): String { - return FloconEncoder.json.encodeToString( - listOf( - item.toSerializable() - ) - ) -} @Serializable internal class AnalyticsItemSerializable( @@ -28,7 +18,7 @@ internal class AnalyticsPropertySerializable( val value: String, ) -internal fun AnalyticsItem.toSerializable(): AnalyticsItemSerializable { +internal fun AnalyticsItem.toRemote(): AnalyticsItemSerializable { return AnalyticsItemSerializable( id = id, analyticsTableId = analyticsTableId, diff --git a/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/AndroidConfig.kt b/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/AndroidConfig.kt new file mode 100644 index 000000000..e25d4d737 --- /dev/null +++ b/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/AndroidConfig.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.buildlogic + +import com.android.build.gradle.LibraryExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +internal fun Project.configureAndroidLibrary() { + extensions.configure { + compileSdk = 36 + + defaultConfig { + minSdk = 23 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + } +} diff --git a/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconAndroidLibraryConventionPlugin.kt b/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconAndroidLibraryConventionPlugin.kt new file mode 100644 index 000000000..af1fb9a27 --- /dev/null +++ b/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconAndroidLibraryConventionPlugin.kt @@ -0,0 +1,26 @@ +package io.github.openflocon.buildlogic + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +class FloconAndroidLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + } + + configureAndroidLibrary() + + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + } + } +} diff --git a/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconKotlinMultiplatformConventionPlugin.kt b/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconKotlinMultiplatformConventionPlugin.kt new file mode 100644 index 000000000..f60e19246 --- /dev/null +++ b/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconKotlinMultiplatformConventionPlugin.kt @@ -0,0 +1,38 @@ +package io.github.openflocon.buildlogic + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +class FloconKotlinMultiplatformConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("org.jetbrains.kotlin.multiplatform") + apply("com.android.library") + } + + configureAndroidLibrary() + + extensions.configure { + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + compilerOptions { + freeCompilerArgs.add("-XXLanguage:+ExpectRefinement") + } + } + } + } +} diff --git a/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconPublishConventionPlugin.kt b/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconPublishConventionPlugin.kt new file mode 100644 index 000000000..df47b1eaf --- /dev/null +++ b/FloconAndroid/build-logic/convention/bin/main/io/github/openflocon/buildlogic/FloconPublishConventionPlugin.kt @@ -0,0 +1,52 @@ +package io.github.openflocon.buildlogic + +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class FloconPublishConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.vanniktech.maven.publish") + } + + extensions.configure { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + pom { + name.set(project.name) + description.set(project.findProperty("floconDescription") as? String) + inceptionYear.set("2025") + url.set("https://github.com/openflocon/Flocon") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + id.set("openflocon") + name.set("Open Flocon") + url.set("https://github.com/openflocon") + } + } + scm { + url.set("https://github.com/openflocon/Flocon") + connection.set("scm:git:git://github.com/openflocon/Flocon.git") + developerConnection.set("scm:git:ssh://git@github.com/openflocon/Flocon.git") + } + } + } + } + } +} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt deleted file mode 100644 index 0705dc523..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ /dev/null @@ -1,73 +0,0 @@ -package io.github.openflocon.flocon.plugins.analytics - -import io.github.openflocon.flocon.FloconConfig -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.FloconPluginFactory -import io.github.openflocon.flocon.Protocol -import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.core.encode -import io.github.openflocon.flocon.plugins.analytics.mapper.toRemote -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem - -class FloconAnalyticsConfig : FloconPluginConfig - -interface FloconAnalyticsPlugin : FloconPlugin { - fun registerAnalytics(analyticsItems: List) -} - -object FloconAnalytics : FloconPluginFactory { - override val name: String = "Analytics" - override val pluginId: String = Protocol.ToDevice.Analytics.Plugin - override fun createConfig(context: FloconContext) = FloconAnalyticsConfig() - override fun install( - pluginConfig: FloconAnalyticsConfig, - floconConfig: FloconConfig, - encoder: FloconEncoder - ): FloconAnalyticsPlugin { - return FloconAnalyticsPluginImpl( - sender = floconConfig.client as FloconMessageSender, - encoder = encoder - ) - } -} - -internal class FloconAnalyticsPluginImpl( - private val sender: FloconMessageSender, - private val encoder: FloconEncoder -) : FloconPlugin, FloconAnalyticsPlugin { - override val key: String - get() = Protocol.ToDevice.Analytics.Plugin - - override suspend fun onMessageReceived( - method: String, - body: String, - ) { - // no op - } - - override suspend fun onConnectedToServer() { - // no op - } - - override fun registerAnalytics(analyticsItems: List) { - sendAnalytics(analyticsItems) - } - - private fun sendAnalytics(analyticsItems: List) { - analyticsItems.takeIf { it.isNotEmpty() }?.forEach { toSend -> - try { - sender.send( - plugin = Protocol.FromDevice.Analytics.Plugin, - method = Protocol.FromDevice.Analytics.Method.AddItems, - body = encoder.encode(listOf(toSend.toRemote())) - ) - } catch (t: Throwable) { - FloconLogger.logError("error on sendAnalytics", t) - } - } - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt deleted file mode 100644 index d4aef5b4d..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt +++ /dev/null @@ -1,32 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package io.github.openflocon.flocon.plugins.analytics.builder - -import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem -import io.github.openflocon.flocon.utils.currentTimeMillis -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid - -class AnalyticsBuilder( - val analyticsTableId: String, - private val analyticsPlugin: FloconAnalyticsPlugin?, -) { - fun logEvents(vararg events: AnalyticsEvent) { - this.logEvents(events.toList()) - } - - fun logEvents(events: List) { - val analyticsItems = events.map { - AnalyticsItem( - id = Uuid.random().toString(), - analyticsTableId = analyticsTableId, - eventName = it.eventName, - createdAt = currentTimeMillis(), - properties = it.properties, - ) - } - analyticsPlugin?.registerAnalytics(analyticsItems) - } -} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt deleted file mode 100644 index 4bab74e3c..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.openflocon.flocon.plugins.analytics.mapper - -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem -import kotlinx.serialization.Serializable - -@Serializable -internal class AnalyticsItemSerializable( - val id: String, - val analyticsTableId: String, - val eventName: String, - val createdAt: Long, - val properties: List, -) - -@Serializable -internal class AnalyticsPropertySerializable( - val name: String, - val value: String, -) - -internal fun AnalyticsItem.toRemote(): AnalyticsItemSerializable { - return AnalyticsItemSerializable( - id = id, - analyticsTableId = analyticsTableId, - eventName = eventName, - createdAt = createdAt, - properties = properties.map { - AnalyticsPropertySerializable( - name = it.name, - value = it.value - ) - } - ) -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt deleted file mode 100644 index b0de88ffa..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.plugins.analytics.model - -data class AnalyticsEvent( - val eventName: String, - val properties: List, -) { - constructor( - eventName: String, - vararg properties: AnalyticsPropertiesConfig, - ) : this(eventName, properties.toList()) -} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt deleted file mode 100644 index 55c7285cd..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.plugins.analytics.model - -data class AnalyticsItem( - val id: String, - val analyticsTableId: String, - val eventName: String, - val createdAt: Long, - val properties: List, -) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt deleted file mode 100644 index f36d72382..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.plugins.analytics.model - -data class AnalyticsPropertiesConfig( - val name: String, - val value: String, -) - -infix fun String.analyticsProperty(value: String) = AnalyticsPropertiesConfig( - name = this, - value = value, -) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt deleted file mode 100644 index 8836baf08..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/FloconNetworkPlugin.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.network - -import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkCallResponse -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketEvent -import io.github.openflocon.flocon.pluginsold.network.model.FloconWebSocketMockListener -import io.github.openflocon.flocon.pluginsold.network.model.MockNetworkResponse - -class FloconNetworkConfig : FloconPluginConfig { - var badQualityConfig: BadQualityConfig? = null - val mocks = mutableListOf() -} - -interface FloconNetworkPlugin : FloconPlugin { - val mocks: Collection - val badQualityConfig: BadQualityConfig? - - fun logRequest(request: FloconNetworkCallRequest) - fun logResponse(response: FloconNetworkCallResponse) - - suspend fun logWebSocket( - event: FloconWebSocketEvent, - ) - - suspend fun registerWebSocketMockListener(id: String, listener: FloconWebSocketMockListener) -} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/BadQualityConfig.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/BadQualityConfig.kt deleted file mode 100644 index 7fe952d6f..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/BadQualityConfig.kt +++ /dev/null @@ -1,77 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.network.model - -import io.github.openflocon.flocon.FloconLogger -import kotlin.random.Random - -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, - ) { - fun shouldSimulateLatency(): Boolean { - return latencyTriggerProbability > 0f && (latencyTriggerProbability == 1f || Random.nextDouble() < latencyTriggerProbability) - } - fun getRandomLatency() : Long { - return Random.nextLong( - minLatencyMs, - maxLatencyMs + 1 - ) - } - } - class Error( - val weight: Float, // increase the probability of being triggered vs all others errors - val type: Type, - ) { - sealed interface Type { - data class Body( - val errorCode: Int, - val errorBody: String, - val errorContentType: String, // "application/json" - ) : Type - data class ErrorThrow( - val classPath: String, - ) : Type { - fun generate() : Throwable? { - return try { - io.github.openflocon.flocon.utils.createThrowableFromClassName(classPath) - } catch (t: Throwable) { - FloconLogger.logError("BadQualityConfig error, className not found", t) - null - } - } - } - } - } - - fun shouldFail(): Boolean { - return errorProbability > 0 && Random.nextDouble() < errorProbability - } - - fun selectRandomError(): 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() - } -} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconHttpRequest.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconHttpRequest.kt deleted file mode 100644 index 5d4af5515..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconHttpRequest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.network.model - -data class FloconNetworkRequest( - val url: String, - val method: String, - val startTime: Long, - val headers: Map, - val body: String?, - val size: Long?, - val isMocked: Boolean, -) - -data class FloconNetworkResponse( - val httpCode: Int?, - val grpcStatus: String?, - val contentType: String?, - val body: String?, - val size: Long?, - val headers: Map, - val requestHeaders: Map?, // we might receive the request headers later if the interceptor is at first position in the http interceptor chain - val error: String?, - val isImage: Boolean, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallRequest.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallRequest.kt deleted file mode 100644 index 8638105a6..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.network.model - -data class FloconNetworkCallRequest( - val floconCallId: String, - val request: FloconNetworkRequest, - val floconNetworkType: String, - val isMocked: Boolean, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallResponse.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallResponse.kt deleted file mode 100644 index 6a8fe9e28..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconNetworkCallResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.network.model - -data class FloconNetworkCallResponse( - val floconCallId: String, - val response: FloconNetworkResponse, - val durationMs: Double, - val floconNetworkType: String, - val isMocked: Boolean, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketEvent.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketEvent.kt deleted file mode 100644 index 29903e336..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketEvent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.network.model - -import io.github.openflocon.flocon.utils.currentTimeMillis - -class FloconWebSocketEvent( - val websocketUrl: String, - val event: Event, - val size: Long = 0L, - val message: String? = null, - val error: Throwable? = null, - val timeStamp: Long = currentTimeMillis(), -) { - enum class Event { - Closed, - Closing, - Error, - ReceiveMessage, - SendMessage, - Open, - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketMockListener.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketMockListener.kt deleted file mode 100644 index 3cd6b177e..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/FloconWebSocketMockListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.network.model - -interface FloconWebSocketMockListener { - fun onMessage(message: String) -} diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/MockNetworkResponse.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/MockNetworkResponse.kt deleted file mode 100644 index a1a51226d..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/network/model/MockNetworkResponse.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.network.model - -data class MockNetworkResponse( - val expectation: Expectation, - val response: Response, -) { - data class Expectation( - val urlPattern: String, // a regex - val method: String, // can be get, post, put, ... or a wildcard * - ) { - - private val regex = Regex(urlPattern) - - fun matches(url: String, method: String): Boolean { - val urlMatches = regex.matches(url) - val methodMatches = this.method == "*" || this.method.equals(method, ignoreCase = true) - return urlMatches && methodMatches - } - } - - sealed interface Response { - val delay: Long - data class Body( - val httpCode: Int, - val body: String, - override val delay: Long, - val mediaType: String, - val headers: Map, - ) : Response - data class ErrorThrow( - val classPath: String, - override val delay: Long, - ) : Response { - fun generate() : Throwable? { - return io.github.openflocon.flocon.utils.createThrowableFromClassName(classPath) - } - } - } -} \ No newline at end of file diff --git a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts index e6a51bc83..a6d942dc0 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts @@ -12,7 +12,7 @@ android { dependencies { - implementation(projects.flocon) + implementation(projects.network.core) implementation(platform(libs.kotlinx.coroutines.bom)) implementation(libs.kotlinx.coroutines.core) diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/BadQuality.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/BadQuality.kt index d69d425ae..d410a2a2d 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/BadQuality.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/BadQuality.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.grpc -import io.github.openflocon.flocon.pluginsold.network.model.BadQualityConfig +import io.github.openflocon.flocon.network.core.model.BadQualityConfig import java.io.IOException @Throws(IOException::class) diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcBaseInterceptor.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcBaseInterceptor.kt index ee83bd28f..4f321f3f1 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcBaseInterceptor.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcBaseInterceptor.kt @@ -5,9 +5,9 @@ package io.github.openflocon.flocon.grpc import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.grpc.model.RequestHolder import io.github.openflocon.flocon.grpc.model.toHeaders -import io.github.openflocon.flocon.pluginsold.network.FloconNetworkPlugin -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkResponse +import io.github.openflocon.flocon.network.core.FloconNetworkPlugin +import io.github.openflocon.flocon.network.core.model.FloconNetworkRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkResponse import io.grpc.CallOptions import io.grpc.Channel import io.grpc.ClientCall diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt index f9cc4f8ad..732a4a925 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt @@ -1,7 +1,7 @@ package io.github.openflocon.flocon.grpc -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkResponse +import io.github.openflocon.flocon.network.core.model.FloconNetworkRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkResponse internal class FloconGrpcPlugin() { diff --git a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt index 00d93ba8b..5ad926c13 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt +++ b/FloconAndroid/grpc/grpc-interceptor-base/src/main/kotlin/io/github/openflocon/flocon/grpc/model/RequestHolder.kt @@ -1,6 +1,6 @@ package io.github.openflocon.flocon.grpc.model -import io.github.openflocon.flocon.pluginsold.network.model.FloconNetworkRequest +import io.github.openflocon.flocon.network.core.model.FloconNetworkRequest import kotlinx.coroutines.CompletableDeferred internal data class RequestHolder( diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index 05691da2a..91a7f5647 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -91,6 +91,9 @@ dependencies { debugImplementation(projects.tables) releaseImplementation(projects.tablesNoOp) + debugImplementation(projects.analytics) + releaseImplementation(projects.analyticsNoOp) + debugImplementation(project(":database:room")) releaseImplementation(project(":database:room-no-op")) debugImplementation(project(":database:room3")) diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index 3f81b8483..c481c4321 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -39,6 +39,8 @@ include(":deeplinks") include(":deeplinks-no-op") include(":tables") include(":tables-no-op") +include(":analytics") +include(":analytics-no-op") include(":network:core") include(":network:core-no-op") include(":database:core") From b8136e48fe096343dcd290db1f2e680451746576 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 13 May 2026 14:51:09 +0200 Subject: [PATCH 31/38] fix: Clean --- .../{plugins => }/analytics/FloconAnalyticsPlugin.kt | 4 ++-- .../analytics/builder/AnalyticsBuilder.kt | 8 ++++---- .../analytics/mapper/AnalyticsItemsMapper.kt | 4 ++-- .../{plugins => }/analytics/model/AnalyticsEvent.kt | 2 +- .../{plugins => }/analytics/model/AnalyticsItem.kt | 2 +- .../analytics/model/AnalyticsPropertiesConfig.kt | 2 +- .../deeplinks/FloconDeeplinkEncoding.kt | 4 ++-- .../{plugins => }/deeplinks/FloconDeeplinks.kt | 4 ++-- .../{plugins => }/deeplinks/FloconDeeplinksConfig.kt | 4 ++-- .../{plugins => }/deeplinks/FloconDeeplinksPlugin.kt | 4 ++-- .../flocon/{plugins => }/deeplinks/Mapping.kt | 12 ++++++------ .../{plugins => }/deeplinks/model/DeeplinkModel.kt | 2 +- .../{plugins => }/deeplinks/model/DeeplinksRemote.kt | 2 +- .../openflocon/flocon/myapplication/MainActivity.kt | 6 +++--- .../flocon/myapplication/table/InitializeTable.kt | 4 ++-- .../flocon/myapplication/multi/MainActivity.kt | 2 +- .../{plugins => }/tables/FloconTablesPlugin.kt | 6 +++--- .../flocon/{plugins => }/tables/dsl/TableItemDsl.kt | 6 +++--- .../{plugins => }/tables/model/TableColumnConfig.kt | 0 .../flocon/{plugins => }/tables/model/TableItem.kt | 2 +- 20 files changed, 40 insertions(+), 40 deletions(-) rename FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/analytics/FloconAnalyticsPlugin.kt (94%) rename FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/analytics/builder/AnalyticsBuilder.kt (74%) rename FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/analytics/mapper/AnalyticsItemsMapper.kt (85%) rename FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/analytics/model/AnalyticsEvent.kt (81%) rename FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/analytics/model/AnalyticsItem.kt (75%) rename FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/analytics/model/AnalyticsPropertiesConfig.kt (77%) rename FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/deeplinks/FloconDeeplinkEncoding.kt (81%) rename FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/deeplinks/FloconDeeplinks.kt (95%) rename FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/deeplinks/FloconDeeplinksConfig.kt (90%) rename FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/deeplinks/FloconDeeplinksPlugin.kt (93%) rename FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/deeplinks/Mapping.kt (72%) rename FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/deeplinks/model/DeeplinkModel.kt (91%) rename FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/deeplinks/model/DeeplinksRemote.kt (95%) rename FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/tables/FloconTablesPlugin.kt (93%) rename FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/tables/dsl/TableItemDsl.kt (83%) rename FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/tables/model/TableColumnConfig.kt (100%) rename FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/{plugins => }/tables/model/TableItem.kt (93%) diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/FloconAnalyticsPlugin.kt similarity index 94% rename from FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt rename to FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/FloconAnalyticsPlugin.kt index 9af8d73c8..dd06edf60 100644 --- a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsPlugin.kt +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/FloconAnalyticsPlugin.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.analytics +package io.github.openflocon.flocon.analytics import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext @@ -10,7 +10,7 @@ import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.core.encode -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.analytics.model.AnalyticsItem class FloconAnalyticsConfig : FloconPluginConfig diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/builder/AnalyticsBuilder.kt similarity index 74% rename from FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt rename to FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/builder/AnalyticsBuilder.kt index d4aef5b4d..4b2387e1d 100644 --- a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/builder/AnalyticsBuilder.kt +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/builder/AnalyticsBuilder.kt @@ -1,10 +1,10 @@ @file:OptIn(ExperimentalUuidApi::class) -package io.github.openflocon.flocon.plugins.analytics.builder +package io.github.openflocon.flocon.analytics.builder -import io.github.openflocon.flocon.plugins.analytics.FloconAnalyticsPlugin -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.analytics.FloconAnalyticsPlugin +import io.github.openflocon.flocon.analytics.model.AnalyticsEvent +import io.github.openflocon.flocon.analytics.model.AnalyticsItem import io.github.openflocon.flocon.utils.currentTimeMillis import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/mapper/AnalyticsItemsMapper.kt similarity index 85% rename from FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt rename to FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/mapper/AnalyticsItemsMapper.kt index fc835947f..cb312861a 100644 --- a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/mapper/AnalyticsItemsMapper.kt +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/mapper/AnalyticsItemsMapper.kt @@ -1,6 +1,6 @@ -package io.github.openflocon.flocon.plugins.analytics.mapper +package io.github.openflocon.flocon.analytics.mapper -import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem +import io.github.openflocon.flocon.analytics.model.AnalyticsItem import kotlinx.serialization.Serializable @Serializable diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsEvent.kt similarity index 81% rename from FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt rename to FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsEvent.kt index b0de88ffa..0de6c0798 100644 --- a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsEvent.kt +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsEvent.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.analytics.model +package io.github.openflocon.flocon.analytics.model data class AnalyticsEvent( val eventName: String, diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsItem.kt similarity index 75% rename from FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt rename to FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsItem.kt index 55c7285cd..ba8c85753 100644 --- a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsItem.kt +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsItem.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.analytics.model +package io.github.openflocon.flocon.analytics.model data class AnalyticsItem( val id: String, diff --git a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsPropertiesConfig.kt similarity index 77% rename from FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt rename to FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsPropertiesConfig.kt index f36d72382..e73421fbb 100644 --- a/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/model/AnalyticsPropertiesConfig.kt +++ b/FloconAndroid/analytics/src/commonMain/kotlin/io/github/openflocon/flocon/analytics/model/AnalyticsPropertiesConfig.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.analytics.model +package io.github.openflocon.flocon.analytics.model data class AnalyticsPropertiesConfig( val name: String, diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinkEncoding.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinkEncoding.kt similarity index 81% rename from FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinkEncoding.kt rename to FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinkEncoding.kt index 45f898644..47c95538c 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinkEncoding.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinkEncoding.kt @@ -1,8 +1,8 @@ -package io.github.openflocon.flocon.plugins.deeplinks +package io.github.openflocon.flocon.deeplinks import io.github.openflocon.flocon.FloconEncoding import io.github.openflocon.flocon.dsl.FloconMarker -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote +import io.github.openflocon.flocon.deeplinks.model.DeeplinkParameterRemote import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinks.kt similarity index 95% rename from FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt rename to FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinks.kt index 856d79497..16ba46096 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinks.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinks.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.deeplinks +package io.github.openflocon.flocon.deeplinks import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext @@ -11,7 +11,7 @@ import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.core.encode import io.github.openflocon.flocon.dsl.FloconMarker -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel +import io.github.openflocon.flocon.deeplinks.model.DeeplinkModel object FloconDeeplinks : FloconPluginFactory { override val name: String = "Deeplinks" diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksConfig.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinksConfig.kt similarity index 90% rename from FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksConfig.kt rename to FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinksConfig.kt index 7dd0e694c..2188911d6 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksConfig.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinksConfig.kt @@ -1,7 +1,7 @@ -package io.github.openflocon.flocon.plugins.deeplinks +package io.github.openflocon.flocon.deeplinks import io.github.openflocon.flocon.FloconPluginConfig -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel +import io.github.openflocon.flocon.deeplinks.model.DeeplinkModel abstract class FloconDeeplinksConfig : FloconPluginConfig { abstract fun variable(name: String, block: DeeplinkVariableBuilder.() -> Unit = {}) diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinksPlugin.kt similarity index 93% rename from FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt rename to FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinksPlugin.kt index 2369b1f7f..935b1a319 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/FloconDeeplinksPlugin.kt @@ -1,7 +1,7 @@ -package io.github.openflocon.flocon.plugins.deeplinks +package io.github.openflocon.flocon.deeplinks import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel +import io.github.openflocon.flocon.deeplinks.model.DeeplinkModel class DeeplinkLinkBuilder internal constructor( private val link: String diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/Mapping.kt similarity index 72% rename from FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt rename to FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/Mapping.kt index 8e0194e42..4fbe5c66d 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/Mapping.kt @@ -1,10 +1,10 @@ -package io.github.openflocon.flocon.plugins.deeplinks +package io.github.openflocon.flocon.deeplinks -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkRemote -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkVariableRemote -import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinksRemote +import io.github.openflocon.flocon.deeplinks.model.DeeplinkModel +import io.github.openflocon.flocon.deeplinks.model.DeeplinkParameterRemote +import io.github.openflocon.flocon.deeplinks.model.DeeplinkRemote +import io.github.openflocon.flocon.deeplinks.model.DeeplinkVariableRemote +import io.github.openflocon.flocon.deeplinks.model.DeeplinksRemote internal fun createRemote( deeplinks: List, diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/model/DeeplinkModel.kt similarity index 91% rename from FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt rename to FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/model/DeeplinkModel.kt index 1578513e1..aa3d60d59 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/model/DeeplinkModel.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.deeplinks.model +package io.github.openflocon.flocon.deeplinks.model @ConsistentCopyVisibility data class DeeplinkModel internal constructor( diff --git a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/model/DeeplinksRemote.kt similarity index 95% rename from FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt rename to FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/model/DeeplinksRemote.kt index 1c9e7a37e..9e5be525e 100644 --- a/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt +++ b/FloconAndroid/deeplinks/src/commonMain/kotlin/io/github/openflocon/flocon/deeplinks/model/DeeplinksRemote.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.deeplinks.model +package io.github.openflocon.flocon.deeplinks.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index 4c26b2858..f808131a7 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -33,9 +33,9 @@ import io.github.openflocon.flocon.myapplication.ui.ImagesListView import io.github.openflocon.flocon.myapplication.ui.theme.MyApplicationTheme import io.github.openflocon.flocon.network.core.FloconNetwork import io.github.openflocon.flocon.okhttp.FloconOkhttpInterceptor -import io.github.openflocon.flocon.plugins.analytics.FloconAnalytics -import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks -import io.github.openflocon.flocon.plugins.tables.FloconTable +import io.github.openflocon.flocon.analytics.FloconAnalytics +import io.github.openflocon.flocon.deeplinks.FloconDeeplinks +import io.github.openflocon.flocon.tables.FloconTable import io.github.openflocon.flocon.startFlocon import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt index 067f69081..a054832b2 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt @@ -1,8 +1,8 @@ package io.github.openflocon.flocon.myapplication.table import io.github.openflocon.flocon.Flocon -import io.github.openflocon.flocon.plugins.tables.dsl.table -import io.github.openflocon.flocon.plugins.tables.tablePlugin +import io.github.openflocon.flocon.tables.dsl.table +import io.github.openflocon.flocon.tables.tablePlugin fun initializeTable() { Flocon.tablePlugin.table("analytics") { diff --git a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt index 89253a000..d2c637325 100644 --- a/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt +++ b/FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/MainActivity.kt @@ -13,7 +13,7 @@ import io.github.openflocon.flocon.myapplication.multi.Databases.getFoodDatabase import io.github.openflocon.flocon.myapplication.multi.database.initializeDatabases import io.github.openflocon.flocon.myapplication.multi.sharedpreferences.initializeSharedPreferences import io.github.openflocon.flocon.myapplication.multi.ui.App -import io.github.openflocon.flocon.plugins.deeplinks.FloconDeeplinks +import io.github.openflocon.flocon.deeplinks.FloconDeeplinks import io.github.openflocon.flocon.startFlocon import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/FloconTablesPlugin.kt similarity index 93% rename from FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt rename to FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/FloconTablesPlugin.kt index e71bf2ee7..05f41b7cd 100644 --- a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesPlugin.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/FloconTablesPlugin.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.tables +package io.github.openflocon.flocon.tables import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.FloconConfig @@ -13,8 +13,8 @@ import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.core.encode import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.error.pluginNotInitialized -import io.github.openflocon.flocon.plugins.tables.model.TableItem -import io.github.openflocon.flocon.plugins.tables.model.toRemote +import io.github.openflocon.flocon.tables.model.TableItem +import io.github.openflocon.flocon.tables.model.toRemote import kotlin.collections.map class FloconTableConfig : FloconPluginConfig diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/dsl/TableItemDsl.kt similarity index 83% rename from FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt rename to FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/dsl/TableItemDsl.kt index 98802bc8a..09fb74fc6 100644 --- a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/dsl/TableItemDsl.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/dsl/TableItemDsl.kt @@ -1,9 +1,9 @@ @file:OptIn(ExperimentalUuidApi::class, ExperimentalTime::class) -package io.github.openflocon.flocon.plugins.tables.dsl +package io.github.openflocon.flocon.tables.dsl -import io.github.openflocon.flocon.plugins.tables.FloconTablePlugin -import io.github.openflocon.flocon.plugins.tables.model.TableItem +import io.github.openflocon.flocon.tables.FloconTablePlugin +import io.github.openflocon.flocon.tables.model.TableItem import io.github.openflocon.flocon.pluginsold.tables.model.TableColumnConfig import kotlin.time.Clock import kotlin.time.ExperimentalTime diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/model/TableColumnConfig.kt similarity index 100% rename from FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableColumnConfig.kt rename to FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/model/TableColumnConfig.kt diff --git a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/model/TableItem.kt similarity index 93% rename from FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt rename to FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/model/TableItem.kt index 9eaff3359..11245d37e 100644 --- a/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/model/TableItem.kt +++ b/FloconAndroid/tables/src/commonMain/kotlin/io/github/openflocon/flocon/tables/model/TableItem.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.tables.model +package io.github.openflocon.flocon.tables.model import io.github.openflocon.flocon.pluginsold.tables.model.TableColumnConfig import kotlinx.serialization.Serializable From 1d2399b4036a3182e6ebd9fb5fe9457cc0383899 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 13 May 2026 14:51:54 +0200 Subject: [PATCH 32/38] fix: Delete duplicate --- .../flocon/plugins/database/FloconDatabasePlugin.kt | 1 - .../plugins/database/model/FloconDatabaseModel.kt | 1 - .../model/fromdevice/DatabaseQueryLogModel.kt | 11 ----------- .../model/fromdevice/DeviceDataBaseDataModel.kt | 9 --------- .../model/fromdevice/QueryResultReceivedDataModel.kt | 9 --------- .../database/model/todevice/DatabaseQueryMessage.kt | 10 ---------- 6 files changed, 41 deletions(-) delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseQueryLogModel.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DeviceDataBaseDataModel.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/QueryResultReceivedDataModel.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/todevice/DatabaseQueryMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt deleted file mode 100644 index 1d9511610..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.kt +++ /dev/null @@ -1 +0,0 @@ -// DEPRECATED: Moved to :database:core \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt deleted file mode 100644 index dc137eed6..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/FloconDatabaseModel.kt +++ /dev/null @@ -1 +0,0 @@ -// DEPRECATED diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseQueryLogModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseQueryLogModel.kt deleted file mode 100644 index d75c31fb5..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DatabaseQueryLogModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.plugins.database.model.fromdevice - -import kotlinx.serialization.Serializable - -@Serializable -internal data class DatabaseQueryLogModel( - val dbName: String, - val sqlQuery: String, - val bindArgs: List?, - val timestamp: Long, -) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DeviceDataBaseDataModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DeviceDataBaseDataModel.kt deleted file mode 100644 index 6a24b7ea9..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/DeviceDataBaseDataModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.plugins.database.model.fromdevice - -import kotlinx.serialization.Serializable - -@Serializable -internal data class DeviceDataBaseDataModel( - val id: String, - val name: String, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/QueryResultReceivedDataModel.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/QueryResultReceivedDataModel.kt deleted file mode 100644 index 66f176021..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/fromdevice/QueryResultReceivedDataModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.plugins.database.model.fromdevice - -import kotlinx.serialization.Serializable - -@Serializable -internal data class QueryResultDataModel( - val requestId: String, - val result: String, -) \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/todevice/DatabaseQueryMessage.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/todevice/DatabaseQueryMessage.kt deleted file mode 100644 index 27508e328..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/database/model/todevice/DatabaseQueryMessage.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.openflocon.flocon.plugins.database.model.todevice - -import kotlinx.serialization.Serializable - -@Serializable -internal data class DatabaseQueryMessage( - val query: String, - val requestId: String, - val database: String, -) \ No newline at end of file From a25e0686924ab91eb50dca8df005a678d3455560 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 13 May 2026 16:03:59 +0200 Subject: [PATCH 33/38] feat: Move crashreporter --- .../crashreporter-no-op/build.gradle.kts | 98 +++++++++++++++++++ .../crashreporter/FloconCrashReporterNoOp.kt | 50 ++++++++++ FloconAndroid/crashreporter/build.gradle.kts | 28 ++++++ .../FloconCrashReporterDataSource.android.kt | 5 +- .../UncaughtExceptionHandler.kt} | 2 +- .../FloconCrashReporterDataSource.kt | 4 +- .../FloconCrashReporterPlugin.kt | 4 +- .../model/CrashReportDataModel.kt | 2 +- .../crashreporter/CrashReporterDataSource.kt | 6 +- .../FloconCrashReporterDataSource.jvm.kt | 8 +- .../FloconCrashReporterDataSource.wasmJs.kt | 4 +- .../FloconCrashReporterDataSource.android.kt | 13 --- .../FloconCrashReporterPlugin.kt | 17 ---- FloconAndroid/settings.gradle.kts | 2 + 14 files changed, 195 insertions(+), 48 deletions(-) create mode 100644 FloconAndroid/crashreporter-no-op/build.gradle.kts create mode 100644 FloconAndroid/crashreporter-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterNoOp.kt create mode 100644 FloconAndroid/crashreporter/build.gradle.kts rename FloconAndroid/{flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold => crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon}/crashreporter/FloconCrashReporterDataSource.android.kt (88%) rename FloconAndroid/{flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/UncaughtExceptionHandler.android.kt => crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/UncaughtExceptionHandler.kt} (91%) rename FloconAndroid/{flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon}/crashreporter/FloconCrashReporterDataSource.kt (76%) rename FloconAndroid/{flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon}/crashreporter/FloconCrashReporterPlugin.kt (96%) rename FloconAndroid/{flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins => crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon}/crashreporter/model/CrashReportDataModel.kt (78%) rename FloconAndroid/{flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins => crashreporter/src/iosMain/kotlin/io/github/openflocon/flocon}/crashreporter/CrashReporterDataSource.kt (75%) rename FloconAndroid/{flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins => crashreporter/src/jvmMain/kotlin/io/github/openflocon/flocon}/crashreporter/FloconCrashReporterDataSource.jvm.kt (89%) rename FloconAndroid/{flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins => crashreporter/src/wasmJsMain/kotlin/io/github/openflocon/flocon}/crashreporter/FloconCrashReporterDataSource.wasmJs.kt (78%) delete mode 100644 FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.android.kt delete mode 100644 FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt diff --git a/FloconAndroid/crashreporter-no-op/build.gradle.kts b/FloconAndroid/crashreporter-no-op/build.gradle.kts new file mode 100644 index 000000000..3fefefb36 --- /dev/null +++ b/FloconAndroid/crashreporter-no-op/build.gradle.kts @@ -0,0 +1,98 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vanniktech.maven.publish) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.coroutines.core) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.crashreporter.noop" + compileSdk = 36 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + + if (project.hasProperty("signing.required") && project.property("signing.required") == "false") { + // Skip signing + } else { + signAllPublications() + } + + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-crashreporter-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + pom { + name = "Flocon CrashReporter No-Op" + description = project.property("floconDescription") as String + inceptionYear = "2025" + url = "https://github.com/openflocon/Flocon" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "openflocon" + name = "Open Flocon" + url = "https://github.com/openflocon" + } + } + scm { + url = "https://github.com/openflocon/Flocon" + connection = "scm:git:git://github.com/openflocon/Flocon.git" + developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git" + } + } +} diff --git a/FloconAndroid/crashreporter-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterNoOp.kt b/FloconAndroid/crashreporter-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterNoOp.kt new file mode 100644 index 000000000..b3ce8e05e --- /dev/null +++ b/FloconAndroid/crashreporter-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterNoOp.kt @@ -0,0 +1,50 @@ +package io.github.openflocon.flocon.crashreporter + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol + +class FloconCrashReporterConfig : FloconPluginConfig { + var catchFatalErrors: Boolean = true +} + +interface FloconCrashReporterPlugin : FloconPlugin { + fun setupCrashHandler() +} + +object FloconCrashReporter : FloconPluginFactory { + override val name: String = "CrashReporter" + override val pluginId: String = Protocol.ToDevice.Analytics.Plugin // Same as real impl + + override fun createConfig(context: FloconContext) = FloconCrashReporterConfig() + + override fun install( + pluginConfig: FloconCrashReporterConfig, + floconConfig: FloconConfig, + encoder: io.github.openflocon.flocon.core.FloconEncoder + ): FloconCrashReporterPlugin { + return FloconCrashReporterNoOpImpl() + } +} + +internal class FloconCrashReporterNoOpImpl : FloconPlugin, FloconCrashReporterPlugin { + override val key: String = "CRASH_REPORTER" + + override fun setupCrashHandler() { + // no op + } + + override suspend fun onConnectedToServer() { + // no op + } + + override suspend fun onMessageReceived( + method: String, + body: String, + ) { + // no op + } +} diff --git a/FloconAndroid/crashreporter/build.gradle.kts b/FloconAndroid/crashreporter/build.gradle.kts new file mode 100644 index 000000000..c8921cc6b --- /dev/null +++ b/FloconAndroid/crashreporter/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("flocon.kotlin.multiplatform") + id("flocon.publish") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.crashreporter" +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-crashreporter", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt b/FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.android.kt similarity index 88% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt rename to FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.android.kt index 927c2a107..c76eac1f6 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterDataSource.android.kt +++ b/FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.android.kt @@ -1,12 +1,11 @@ -package io.github.openflocon.flocon.pluginsold.crashreporter +package io.github.openflocon.flocon.crashreporter import android.content.Context import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.decode import io.github.openflocon.flocon.core.encode -import io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporterDataSource -import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel +import io.github.openflocon.flocon.crashreporter.model.CrashReportDataModel import java.io.File internal class FloconCrashReporterDataSourceAndroid( diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/UncaughtExceptionHandler.android.kt b/FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/UncaughtExceptionHandler.kt similarity index 91% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/UncaughtExceptionHandler.android.kt rename to FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/UncaughtExceptionHandler.kt index 1608a2c24..9ceea95c8 100644 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/UncaughtExceptionHandler.android.kt +++ b/FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/UncaughtExceptionHandler.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.pluginsold.crashreporter +package io.github.openflocon.flocon.crashreporter import android.os.Build import io.github.openflocon.flocon.FloconContext diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.kt b/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.kt similarity index 76% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.kt rename to FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.kt index 1134b5554..09594503a 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.kt +++ b/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.kt @@ -1,7 +1,7 @@ -package io.github.openflocon.flocon.plugins.crashreporter +package io.github.openflocon.flocon.crashreporter import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel +import io.github.openflocon.flocon.crashreporter.model.CrashReportDataModel internal interface FloconCrashReporterDataSource { fun saveCrash(crash: CrashReportDataModel) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterPlugin.kt similarity index 96% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt rename to FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterPlugin.kt index 8fb7032d3..c3d33a938 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterPlugin.kt +++ b/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterPlugin.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.crashreporter +package io.github.openflocon.flocon.crashreporter import io.github.openflocon.flocon.FloconConfig import io.github.openflocon.flocon.FloconContext @@ -9,7 +9,7 @@ import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.core.FloconMessageSender -import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel +import io.github.openflocon.flocon.crashreporter.model.CrashReportDataModel import io.github.openflocon.flocon.utils.currentTimeMillis import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportDataModel.kt b/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/model/CrashReportDataModel.kt similarity index 78% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportDataModel.kt rename to FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/model/CrashReportDataModel.kt index 1d10cae2f..edbd8cc4d 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/model/CrashReportDataModel.kt +++ b/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/model/CrashReportDataModel.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.flocon.plugins.crashreporter.model +package io.github.openflocon.flocon.crashreporter.model import kotlinx.serialization.Serializable diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/CrashReporterDataSource.kt b/FloconAndroid/crashreporter/src/iosMain/kotlin/io/github/openflocon/flocon/crashreporter/CrashReporterDataSource.kt similarity index 75% rename from FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/CrashReporterDataSource.kt rename to FloconAndroid/crashreporter/src/iosMain/kotlin/io/github/openflocon/flocon/crashreporter/CrashReporterDataSource.kt index 4d80478b4..f2350c2f1 100644 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/CrashReporterDataSource.kt +++ b/FloconAndroid/crashreporter/src/iosMain/kotlin/io/github/openflocon/flocon/crashreporter/CrashReporterDataSource.kt @@ -1,8 +1,8 @@ -package io.github.openflocon.flocon.plugins.crashreporter +package io.github.openflocon.flocon.crashreporter -import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel +import io.github.openflocon.flocon.crashreporter.model.CrashReportDataModel -internal actual fun buildFloconCrashReporterDataSource(context: io.github.openflocon.flocon.FloconContext): io.github.openflocon.flocon.plugins.crashreporter.FloconCrashReporterDataSource { +internal actual fun buildFloconCrashReporterDataSource(context: io.github.openflocon.flocon.FloconContext): io.github.openflocon.flocon.crashreporter.FloconCrashReporterDataSource { return object : FloconCrashReporterDataSource { override fun saveCrash(crash: CrashReportDataModel) { // no op diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.jvm.kt b/FloconAndroid/crashreporter/src/jvmMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.jvm.kt similarity index 89% rename from FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.jvm.kt rename to FloconAndroid/crashreporter/src/jvmMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.jvm.kt index 7b62ac334..b81678f37 100644 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.jvm.kt +++ b/FloconAndroid/crashreporter/src/jvmMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.jvm.kt @@ -1,10 +1,10 @@ -package io.github.openflocon.flocon.plugins.crashreporter +package io.github.openflocon.flocon.crashreporter import io.github.openflocon.flocon.FloconContext import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel -import io.github.openflocon.flocon.plugins.crashreporter.model.crashReportFromJson -import io.github.openflocon.flocon.plugins.crashreporter.model.toJson +import io.github.openflocon.flocon.crashreporter.model.CrashReportDataModel +import io.github.openflocon.flocon.crashreporter.model.crashReportFromJson +import io.github.openflocon.flocon.crashreporter.model.toJson import java.io.File internal class FloconCrashReporterDataSourceJvm : FloconCrashReporterDataSource { diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.wasmJs.kt b/FloconAndroid/crashreporter/src/wasmJsMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.wasmJs.kt similarity index 78% rename from FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.wasmJs.kt rename to FloconAndroid/crashreporter/src/wasmJsMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.wasmJs.kt index c35073b68..199337f59 100644 --- a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.wasmJs.kt +++ b/FloconAndroid/crashreporter/src/wasmJsMain/kotlin/io/github/openflocon/flocon/crashreporter/FloconCrashReporterDataSource.wasmJs.kt @@ -1,7 +1,7 @@ -package io.github.openflocon.flocon.plugins.crashreporter +package io.github.openflocon.flocon.crashreporter import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.crashreporter.model.CrashReportDataModel +import io.github.openflocon.flocon.crashreporter.model.CrashReportDataModel internal actual fun buildFloconCrashReporterDataSource(context: FloconContext): FloconCrashReporterDataSource = object : FloconCrashReporterDataSource { override fun saveCrash(crash: CrashReportDataModel) {} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.android.kt b/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.android.kt deleted file mode 100644 index e736d8c8b..000000000 --- a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/crashreporter/FloconCrashReporterDataSource.android.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.openflocon.flocon.plugins.crashreporter - -import io.github.openflocon.flocon.FloconContext - -internal actual fun buildFloconCrashReporterDataSource(context: FloconContext): FloconCrashReporterDataSource { - TODO("Not yet implemented") -} - -internal actual fun setupUncaughtExceptionHandler( - context: FloconContext, - onCrash: (Throwable) -> Unit -) { -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt deleted file mode 100644 index e30e0037d..000000000 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/crashreporter/FloconCrashReporterPlugin.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.openflocon.flocon.pluginsold.crashreporter - -import io.github.openflocon.flocon.FloconPlugin -import io.github.openflocon.flocon.FloconPluginConfig - -class FloconCrashReporterConfig : FloconPluginConfig { - var catchFatalErrors: Boolean = true -} - -/** - * Flocon Crash Reporter Plugin. - */ -//expect object FloconCrashReporter : FloconPluginFactory -// -interface FloconCrashReporterPlugin : FloconPlugin { - fun setupCrashHandler() -} \ No newline at end of file diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index c481c4321..e9b0f8524 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -41,6 +41,8 @@ include(":tables") include(":tables-no-op") include(":analytics") include(":analytics-no-op") +include(":crashreporter") +include(":crashreporter-no-op") include(":network:core") include(":network:core-no-op") include(":database:core") From 0f225557f75c4272e557997f2614dd525b94459e Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Thu, 21 May 2026 14:14:13 +0200 Subject: [PATCH 34/38] fix: Build --- .../plugins/analytics/FloconAnalyticsNoOp.kt | 4 +- .../build-logic/convention/build.gradle.kts | 6 + .../crashreporter-no-op/build.gradle.kts | 75 +---------- .../FloconCrashReporterDataSource.android.kt | 19 ++- .../crashreporter/UncaughtExceptionHandler.kt | 37 +++--- .../model/CrashReportDataModel.kt | 12 +- .../database/room/floconRegisterDatabase.kt | 9 -- .../database/room3/floconRegisterDatabase.kt | 9 -- .../datastores-no-op/build.gradle.kts | 1 + FloconAndroid/datastores/build.gradle.kts | 1 + .../plugins/deeplinks/FloconDeeplinksNoOp.kt | 74 +++++++---- FloconAndroid/device-no-op/build.gradle.kts | 27 ++++ .../plugins/device/FloconDevicePlugin.kt | 47 +++++++ FloconAndroid/device/build.gradle.kts | 28 ++++ .../device/FloconDevicePluginImpl.android.kt | 0 .../plugins/device/GetAppIconUtils.android.kt | 0 .../plugins/device/FloconDevicePluginImpl.kt | 0 .../flocon/plugins/device/GetAppIconUtils.kt | 0 .../fromdevice/RegisterDeviceDataModel.kt | 0 .../device/FloconDevicePluginImpl.ios.kt | 0 .../plugins/device/GetAppIconUtils.ios.kt | 0 .../device/FloconDevicePluginImpl.jvm.kt | 0 .../plugins/device/GetAppIconUtils.jvm.kt | 0 .../device/FloconDevicePluginImpl.wasmJs.kt | 0 .../plugins/device/GetAppIconUtils.wasmJs.kt | 0 FloconAndroid/files-no-op/build.gradle.kts | 27 ++++ .../flocon/plugins/files/FloconFilesPlugin.kt | 43 ++++++ FloconAndroid/files/build.gradle.kts | 28 ++++ .../files/FloconFilesPlugin.android.kt | 0 .../files/FloconFilesPlugin.android.kt | 0 .../flocon/plugins/files/FloconFilesPlugin.kt | 0 .../files/model/fromdevice/FileDataModel.kt | 0 .../model/fromdevice/FilesResultDataModel.kt | 0 .../todevice/ToDeviceDeleteFileMessage.kt | 0 .../todevice/ToDeviceDeleteFilesMessage.kt | 0 .../ToDeviceDeleteFolderContentMessage.kt | 0 .../model/todevice/ToDeviceGetFileMessage.kt | 0 .../model/todevice/ToDeviceGetFilesMessage.kt | 0 .../pluginsold/files/FloconFilesPlugin.kt | 0 .../plugins/files/FloconFilesPlugin.ios.kt | 0 .../plugins/files/FloconFilesPlugin.jvm.kt | 0 .../plugins/files/FloconFilesPlugin.wasmJs.kt | 0 .../openflocon/flocon/Flocon.android.kt | 2 +- .../sharedprefs/FloconSharedPreference.kt | 26 ---- .../flocon/core/FloconFileSender.kt | 2 +- .../network/core-no-op/build.gradle.kts | 3 + .../flocon/network/core/noop/FloconNetwork.kt | 8 +- .../core/noop/mapper/BadQualityToJson.kt | 104 --------------- .../noop/mapper/FloconNetworkRequestToJson.kt | 111 ---------------- .../core/noop/mapper/MockResponseToJson.kt | 123 ------------------ .../network/core/noop/mapper/Websocket.kt | 29 ----- .../sample-android-only/build.gradle.kts | 3 + FloconAndroid/settings.gradle.kts | 6 + .../sharedprefs-no-op/build.gradle.kts | 27 ++++ .../sharedprefs/FloconSharedPrefsPlugin.kt | 48 +++++++ .../model/FloconSharedPreferenceModel.kt | 4 + .../sharedprefs/FloconSharedPrefsPlugin.kt | 50 +++++++ .../model/FloconSharedPreferenceModel.kt | 4 + FloconAndroid/sharedprefs/build.gradle.kts | 28 ++++ .../FloconSharedPrefsPlugin.android.kt | 0 .../sharedprefs/FloconSharedPreference.kt | 0 .../FloconSharedPrefsPlugin.android.kt | 0 .../sharedprefs/SharedPreferencesFinder.kt | 0 .../sharedprefs/FloconSharedPrefsPlugin.kt | 0 .../model/FloconPreferenceWrapper.kt | 0 .../model/FloconSharedPreferenceModel.kt | 0 .../fromdevice/PreferenceRowDataModel.kt | 0 .../SharedPreferenceValueResultDataModel.kt | 0 ...oDeviceEditSharedPreferenceValueMessage.kt | 0 ...ToDeviceGetSharedPreferenceValueMessage.kt | 0 .../todevice/ToDeviceGetSharedPrefsMessage.kt | 0 .../sharedprefs/FloconSharedPrefsPlugin.kt | 0 .../buildFloconPreferencesDataSource.kt | 0 .../sharedprefs/model/FloconPreference.kt | 0 .../model/FloconSharedPreferenceModel.kt | 0 .../FloconSharedPrefsPlugin.ios.kt | 0 .../FloconSharedPrefsPlugin.jvm.kt | 0 .../FloconSharedPrefsPlugin.wasmJs.kt | 0 .../flocon/plugins/tables/FloconTablesNoOp.kt | 4 +- 79 files changed, 486 insertions(+), 543 deletions(-) delete mode 100644 FloconAndroid/database/room-no-op/src/main/java/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt delete mode 100644 FloconAndroid/database/room3-no-op/src/main/java/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt create mode 100644 FloconAndroid/device-no-op/build.gradle.kts create mode 100644 FloconAndroid/device-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt create mode 100644 FloconAndroid/device/build.gradle.kts rename FloconAndroid/{flocon => device}/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt (100%) rename FloconAndroid/{flocon => device}/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt (100%) rename FloconAndroid/{flocon => device}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt (100%) rename FloconAndroid/{flocon => device}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.kt (100%) rename FloconAndroid/{flocon => device}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/model/fromdevice/RegisterDeviceDataModel.kt (100%) rename FloconAndroid/{flocon => device}/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.ios.kt (100%) rename FloconAndroid/{flocon => device}/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.ios.kt (100%) rename FloconAndroid/{flocon => device}/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.jvm.kt (100%) rename FloconAndroid/{flocon => device}/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.jvm.kt (100%) rename FloconAndroid/{flocon => device}/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt (100%) rename FloconAndroid/{flocon => device}/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt (100%) create mode 100644 FloconAndroid/files-no-op/build.gradle.kts create mode 100644 FloconAndroid/files-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt create mode 100644 FloconAndroid/files/build.gradle.kts rename FloconAndroid/{flocon => files}/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt (100%) rename FloconAndroid/{flocon => files}/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FileDataModel.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt (100%) rename FloconAndroid/{flocon => files}/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt (100%) rename FloconAndroid/{flocon => files}/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt (100%) rename FloconAndroid/{flocon => files}/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.jvm.kt (100%) rename FloconAndroid/{flocon => files}/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt (100%) delete mode 100644 FloconAndroid/flocon-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt delete mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt delete mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt delete mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt delete mode 100644 FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt create mode 100644 FloconAndroid/sharedprefs-no-op/build.gradle.kts create mode 100644 FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt create mode 100644 FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt create mode 100644 FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt create mode 100644 FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt create mode 100644 FloconAndroid/sharedprefs/build.gradle.kts rename FloconAndroid/{flocon => sharedprefs}/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPreference.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/SharedPreferencesFinder.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/PreferenceRowDataModel.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconPreference.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt (100%) rename FloconAndroid/{flocon => sharedprefs}/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt (100%) diff --git a/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsNoOp.kt b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsNoOp.kt index 3e4204f84..bd8ff2d81 100644 --- a/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsNoOp.kt +++ b/FloconAndroid/analytics-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/analytics/FloconAnalyticsNoOp.kt @@ -6,6 +6,7 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsItem class FloconAnalyticsConfig : FloconPluginConfig @@ -20,7 +21,8 @@ object FloconAnalytics : FloconPluginFactory try { - encoder.decode(file.readText()) + crashReportFromJson(file.readText()) } catch (t: Throwable) { t.printStackTrace() null @@ -55,6 +54,6 @@ internal class FloconCrashReporterDataSourceAndroid( } } -//internal actual fun buildFloconCrashReporterDataSource(context: FloconContext): FloconCrashReporterDataSource { -// return FloconCrashReporterDataSourceAndroid(context.context) -//} +internal actual fun buildFloconCrashReporterDataSource(context: FloconContext): FloconCrashReporterDataSource { + return FloconCrashReporterDataSourceAndroid(context.context) +} diff --git a/FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/UncaughtExceptionHandler.kt b/FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/UncaughtExceptionHandler.kt index 9ceea95c8..7b15f98c5 100644 --- a/FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/UncaughtExceptionHandler.kt +++ b/FloconAndroid/crashreporter/src/androidMain/kotlin/io/github/openflocon/flocon/crashreporter/UncaughtExceptionHandler.kt @@ -1,23 +1,22 @@ package io.github.openflocon.flocon.crashreporter -import android.os.Build import io.github.openflocon.flocon.FloconContext -//internal actual fun setupUncaughtExceptionHandler( -// context: FloconContext, -// onCrash: (Throwable) -> Unit -//) { -// val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() -// -// Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> -// try { -// // Save crash -// onCrash(throwable) -// } catch (t: Throwable) { -// t.printStackTrace() -// } finally { -// // Call original handler to let the app crash normally -// defaultHandler?.uncaughtException(thread, throwable) -// } -// } -//} +internal actual fun setupUncaughtExceptionHandler( + context: FloconContext, + onCrash: (Throwable) -> Unit +) { + val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() + + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + try { + // Save crash + onCrash(throwable) + } catch (t: Throwable) { + t.printStackTrace() + } finally { + // Call original handler to let the app crash normally + defaultHandler?.uncaughtException(thread, throwable) + } + } +} diff --git a/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/model/CrashReportDataModel.kt b/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/model/CrashReportDataModel.kt index edbd8cc4d..be0d36ac1 100644 --- a/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/model/CrashReportDataModel.kt +++ b/FloconAndroid/crashreporter/src/commonMain/kotlin/io/github/openflocon/flocon/crashreporter/model/CrashReportDataModel.kt @@ -1,6 +1,8 @@ package io.github.openflocon.flocon.crashreporter.model import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.encodeToString @Serializable data class CrashReportDataModel( @@ -9,4 +11,12 @@ data class CrashReportDataModel( val exceptionType: String, val exceptionMessage: String, val stackTrace: String, -) \ No newline at end of file +) + +fun CrashReportDataModel.toJson(): String { + return Json.encodeToString(this) +} + +fun crashReportFromJson(json: String): CrashReportDataModel { + return Json.decodeFromString(json) +} \ No newline at end of file diff --git a/FloconAndroid/database/room-no-op/src/main/java/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt b/FloconAndroid/database/room-no-op/src/main/java/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt deleted file mode 100644 index 1aefa1497..000000000 --- a/FloconAndroid/database/room-no-op/src/main/java/io/github/openflocon/flocon/database/room/floconRegisterDatabase.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.database.room - -fun floconRegisterDatabase(displayName: String, database: Any) { - // no op -} - -fun floconRegisterDatabase(displayName: String, openHelper: Any, dummy: Boolean = true) { - // no op -} diff --git a/FloconAndroid/database/room3-no-op/src/main/java/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt b/FloconAndroid/database/room3-no-op/src/main/java/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt deleted file mode 100644 index e5f4b0a89..000000000 --- a/FloconAndroid/database/room3-no-op/src/main/java/io/github/openflocon/flocon/database/room3/floconRegisterDatabase.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.openflocon.flocon.database.room3 - -fun floconRegisterDatabase(displayName: String, database: Any) { - // no op -} - -fun floconRegisterDatabase(displayName: String, openHelper: Any, dummy: Boolean = true) { - // no op -} diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index b27fdd579..e65ef303d 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -14,6 +14,7 @@ kotlin { val commonMain by getting { dependencies { implementation(projects.flocon) + implementation(projects.sharedprefs) implementation(libs.kotlinx.coroutines.core) } } diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 3fa34912b..afcdc2b4c 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -14,6 +14,7 @@ kotlin { val commonMain by getting { dependencies { implementation(projects.flocon) + implementation(projects.sharedprefs) implementation(libs.kotlinx.coroutines.core) } } diff --git a/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt b/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt index 83c349e12..daf00d0ed 100644 --- a/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt +++ b/FloconAndroid/deeplinks-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksNoOp.kt @@ -1,34 +1,62 @@ -package io.github.openflocon.flocon.plugins.deeplinks +package io.github.openflocon.flocon.deeplinks -import io.github.openflocon.flocon.* +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.core.FloconEncoder -actual object FloconDeeplinks : FloconPluginFactory { - override val name: String = "Deeplinks" - override val pluginId: String = null - override fun createConfig() = FloconDeeplinksConfig() - override fun install(config: FloconDeeplinksConfig, app: FloconApp): FloconDeeplinksPlugin { - return FloconDeeplinksPluginNoOp - } +class DeeplinkModel + +class DeeplinkVariable + +class DeeplinkLinkBuilder internal constructor(private val link: String) { + var label: String? = null + var description: String? = null + + infix fun String.withAutoComplete(suggestions: List) {} + infix fun String.withVariable(variableName: String) {} } -private object FloconDeeplinksPluginNoOp : FloconDeeplinksPlugin { - override fun registerDeeplinks(deeplinks: List) { - // no-op - } +class DeeplinkVariableBuilder internal constructor(private val name: String) { + var description: String? = null + fun autoComplete(suggestions: List) {} +} - override fun onMessageReceived(method: String, body: String) { - // no-op - } +abstract class FloconDeeplinksConfig : FloconPluginConfig { + abstract fun variable(name: String, block: DeeplinkVariableBuilder.() -> Unit = {}) + abstract fun deeplink(link: String, block: DeeplinkLinkBuilder.() -> Unit = {}) + internal abstract fun deeplinks(): List + internal abstract fun variables(): List +} - override fun onConnectedToServer() { - // no-op - } +internal class FloconDeeplinksConfigImpl : FloconDeeplinksConfig() { + override fun variable(name: String, block: DeeplinkVariableBuilder.() -> Unit) {} + override fun deeplink(link: String, block: DeeplinkLinkBuilder.() -> Unit) {} + override fun deeplinks(): List = emptyList() + override fun variables(): List = emptyList() } -fun floconRegisterDeeplink(vararg deeplinks: String) { - // no-op +interface FloconDeeplinksPlugin : FloconPlugin + +object FloconDeeplinks : FloconPluginFactory { + override val name: String = "Deeplinks" + override val pluginId: String = "FloconDeeplinks" + + override fun createConfig(context: FloconContext): FloconDeeplinksConfig = FloconDeeplinksConfigImpl() + + override fun install( + pluginConfig: FloconDeeplinksConfig, + floconConfig: FloconConfig, + encoder: FloconEncoder + ): FloconDeeplinksPlugin { + return FloconDeeplinksPluginNoOp + } } -fun floconRegisterDeeplinks(deeplinks: List) { - // no-op +private object FloconDeeplinksPluginNoOp : FloconDeeplinksPlugin { + override val key: String = "DEEP_LINK" + override suspend fun onMessageReceived(method: String, body: String) {} + override suspend fun onConnectedToServer() {} } diff --git a/FloconAndroid/device-no-op/build.gradle.kts b/FloconAndroid/device-no-op/build.gradle.kts new file mode 100644 index 000000000..44390215f --- /dev/null +++ b/FloconAndroid/device-no-op/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("flocon.kotlin.multiplatform") + id("flocon.publish") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.coroutines.core) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.device.noop" +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-device-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/device-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt b/FloconAndroid/device-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt new file mode 100644 index 000000000..6e7bcd408 --- /dev/null +++ b/FloconAndroid/device-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePlugin.kt @@ -0,0 +1,47 @@ +package io.github.openflocon.flocon.plugins.device + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder + +class FloconDeviceConfig : FloconPluginConfig + +interface FloconDevicePlugin : FloconPlugin { + fun registerWithSerial(serial: String) +} + +object FloconDevice : FloconPluginFactory { + override val name: String = "Device" + override val pluginId: String = Protocol.ToDevice.Device.Plugin + override fun createConfig(context: FloconContext) = FloconDeviceConfig() + override fun install( + pluginConfig: FloconDeviceConfig, + floconConfig: FloconConfig, + encoder: FloconEncoder + ): FloconDevicePlugin { + return FloconDevicePluginNoOpImpl() + } +} + +internal class FloconDevicePluginNoOpImpl : FloconPlugin, FloconDevicePlugin { + override val key: String = "DEVICE" + + override fun registerWithSerial(serial: String) { + // no op + } + + override suspend fun onMessageReceived( + method: String, + body: String, + ) { + // no op + } + + override suspend fun onConnectedToServer() { + // no op + } +} diff --git a/FloconAndroid/device/build.gradle.kts b/FloconAndroid/device/build.gradle.kts new file mode 100644 index 000000000..1bbf9f756 --- /dev/null +++ b/FloconAndroid/device/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("flocon.kotlin.multiplatform") + id("flocon.publish") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.device" +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-device", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt b/FloconAndroid/device/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt similarity index 100% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt rename to FloconAndroid/device/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.android.kt diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt b/FloconAndroid/device/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt similarity index 100% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt rename to FloconAndroid/device/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.android.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt b/FloconAndroid/device/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt rename to FloconAndroid/device/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.kt b/FloconAndroid/device/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.kt rename to FloconAndroid/device/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/model/fromdevice/RegisterDeviceDataModel.kt b/FloconAndroid/device/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/model/fromdevice/RegisterDeviceDataModel.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/model/fromdevice/RegisterDeviceDataModel.kt rename to FloconAndroid/device/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/device/model/fromdevice/RegisterDeviceDataModel.kt diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.ios.kt b/FloconAndroid/device/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.ios.kt similarity index 100% rename from FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.ios.kt rename to FloconAndroid/device/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.ios.kt diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.ios.kt b/FloconAndroid/device/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.ios.kt similarity index 100% rename from FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.ios.kt rename to FloconAndroid/device/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.ios.kt diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.jvm.kt b/FloconAndroid/device/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.jvm.kt similarity index 100% rename from FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.jvm.kt rename to FloconAndroid/device/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.jvm.kt diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.jvm.kt b/FloconAndroid/device/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.jvm.kt similarity index 100% rename from FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.jvm.kt rename to FloconAndroid/device/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.jvm.kt diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt b/FloconAndroid/device/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt similarity index 100% rename from FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt rename to FloconAndroid/device/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.wasmJs.kt diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt b/FloconAndroid/device/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt similarity index 100% rename from FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt rename to FloconAndroid/device/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.wasmJs.kt diff --git a/FloconAndroid/files-no-op/build.gradle.kts b/FloconAndroid/files-no-op/build.gradle.kts new file mode 100644 index 000000000..558379a24 --- /dev/null +++ b/FloconAndroid/files-no-op/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("flocon.kotlin.multiplatform") + id("flocon.publish") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.coroutines.core) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.files.noop" +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-files-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/files-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/files-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt new file mode 100644 index 000000000..7fb37ec01 --- /dev/null +++ b/FloconAndroid/files-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt @@ -0,0 +1,43 @@ +package io.github.openflocon.flocon.plugins.files + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder + +class FloconFilesConfig : FloconPluginConfig { + val roots = mutableListOf() +} + +interface FloconFilesPlugin : FloconPlugin + +object FloconFiles : FloconPluginFactory { + override val name: String = "Files" + override val pluginId: String = Protocol.ToDevice.Files.Plugin + override fun createConfig(context: FloconContext) = FloconFilesConfig() + override fun install( + pluginConfig: FloconFilesConfig, + floconConfig: FloconConfig, + encoder: FloconEncoder + ): FloconFilesPlugin { + return FloconFilesPluginNoOpImpl() + } +} + +internal class FloconFilesPluginNoOpImpl : FloconPlugin, FloconFilesPlugin { + override val key: String = "FILES" + + override suspend fun onMessageReceived( + method: String, + body: String, + ) { + // no op + } + + override suspend fun onConnectedToServer() { + // no op + } +} diff --git a/FloconAndroid/files/build.gradle.kts b/FloconAndroid/files/build.gradle.kts new file mode 100644 index 000000000..1686a517e --- /dev/null +++ b/FloconAndroid/files/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("flocon.kotlin.multiplatform") + id("flocon.publish") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.files" +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-files", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt b/FloconAndroid/files/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt similarity index 100% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt rename to FloconAndroid/files/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.android.kt diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt b/FloconAndroid/files/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt similarity index 100% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt rename to FloconAndroid/files/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.android.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FileDataModel.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FileDataModel.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FileDataModel.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FileDataModel.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/fromdevice/FilesResultDataModel.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFileMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFilesMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceDeleteFolderContentMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFileMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/files/model/todevice/ToDeviceGetFilesMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt b/FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt rename to FloconAndroid/files/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/files/FloconFilesPlugin.kt diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt b/FloconAndroid/files/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt similarity index 100% rename from FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt rename to FloconAndroid/files/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.jvm.kt b/FloconAndroid/files/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.jvm.kt similarity index 100% rename from FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.jvm.kt rename to FloconAndroid/files/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.jvm.kt diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt b/FloconAndroid/files/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt similarity index 100% rename from FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt rename to FloconAndroid/files/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.wasmJs.kt diff --git a/FloconAndroid/flocon-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.android.kt b/FloconAndroid/flocon-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.android.kt index 7e6344375..9961ece8d 100644 --- a/FloconAndroid/flocon-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.android.kt +++ b/FloconAndroid/flocon-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/Flocon.android.kt @@ -12,7 +12,7 @@ actual object Flocon : FloconApp() { // This is a no-op implementation @Suppress("UNUSED_PARAMETER") fun initialize(context: Context) { - initializeFlocon() + // no-op } } diff --git a/FloconAndroid/flocon-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt b/FloconAndroid/flocon-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt deleted file mode 100644 index d5be3322c..000000000 --- a/FloconAndroid/flocon-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.openflocon.flocon.plugins.sharedprefs - -import android.content.SharedPreferences -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference -import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreferenceValue - -data class FloconSharedPreference( - override val name: String, - val sharedPreferences: SharedPreferences, -) : FloconPreference { - - override suspend fun set( - columnName: String, - value: FloconPreferenceValue - ) { - // no op - } - - override suspend fun columns(): List { - return emptyList() // no op - } - - override suspend fun get(columnName: String): FloconPreferenceValue? { - return null // no op - } -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt index 43924d572..0dbac9617 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconFileSender.kt @@ -4,7 +4,7 @@ import io.github.openflocon.flocon.FloconFile import io.github.openflocon.flocon.dsl.FloconMarker import io.github.openflocon.flocon.model.FloconFileInfo -internal interface FloconFileSender { +interface FloconFileSender { @FloconMarker fun send(file: FloconFile, infos: FloconFileInfo) diff --git a/FloconAndroid/network/core-no-op/build.gradle.kts b/FloconAndroid/network/core-no-op/build.gradle.kts index 7247ac2a8..5438ed867 100644 --- a/FloconAndroid/network/core-no-op/build.gradle.kts +++ b/FloconAndroid/network/core-no-op/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("flocon.kotlin.multiplatform") id("flocon.publish") + alias(libs.plugins.kotlin.serialization) } kotlin { @@ -13,7 +14,9 @@ kotlin { val commonMain by getting { dependencies { implementation(projects.flocon) + implementation(projects.network.core) implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) } } diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt index 4935b04a4..cdf9cebd7 100644 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt +++ b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/FloconNetwork.kt @@ -10,16 +10,20 @@ import io.github.openflocon.flocon.network.core.noop.plugin.FloconNetworkPluginI import io.github.openflocon.flocon.network.core.FloconNetworkConfig import io.github.openflocon.flocon.network.core.FloconNetworkPlugin +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.core.FloconEncoder + object FloconNetwork : FloconPluginFactory { override val name: String = "Network" override val pluginId: String = Protocol.ToDevice.Network.Plugin - override fun createConfig() = FloconNetworkConfig() + override fun createConfig(context: FloconContext) = FloconNetworkConfig() @OptIn(FloconMarker::class) override fun install( pluginConfig: FloconNetworkConfig, - floconConfig: FloconConfig + floconConfig: FloconConfig, + encoder: FloconEncoder ): FloconNetworkPlugin { return FloconNetworkPluginImpl() .also { FloconNetworkPluginImpl.plugin = it } diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt deleted file mode 100644 index 90df55c94..000000000 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/BadQualityToJson.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io.github.openflocon.flocon.network.core.noop.mapper - -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.network.core.model.BadQualityConfig -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString - -internal fun BadQualityConfig.toJsonString(): String { - return FloconEncoder.json.encodeToString( - toSerializable() - ) -} - -internal fun parseBadQualityConfig(jsonString: String): BadQualityConfig? { - return try { - val parsed = FloconEncoder.json.decodeFromString( - jsonString - ) - parsed.toDomain() - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "bad connection network parsing issue", t) - null - } -} - -@Serializable -internal class BadQualityConfigSerializable( - val latency: LatencySerializable, - val errorProbability: Double, - val errors: List, -) { - @Serializable - class LatencySerializable( - val latencyTriggerProbability: Float, - val minLatencyMs: Long, - val maxLatencyMs: Long, - ) - - @Serializable - class ErrorSerializable( - val weight: Float, - val errorCode: Int? = null, - val errorBody: String? = null, - val errorContentType: String? = null, - val errorException: String? = null, - ) -} - -internal fun BadQualityConfig.toSerializable(): BadQualityConfigSerializable { - return BadQualityConfigSerializable( - latency = BadQualityConfigSerializable.LatencySerializable( - latencyTriggerProbability = latency.latencyTriggerProbability, - minLatencyMs = latency.minLatencyMs, - maxLatencyMs = latency.maxLatencyMs - ), - errorProbability = errorProbability, - errors = errors.map { error -> - when (val t = error.type) { - is BadQualityConfig.Error.Type.Body -> BadQualityConfigSerializable.ErrorSerializable( - weight = error.weight, - errorCode = t.errorCode, - errorBody = t.errorBody, - errorContentType = t.errorContentType - ) - - is BadQualityConfig.Error.Type.ErrorThrow -> BadQualityConfigSerializable.ErrorSerializable( - weight = error.weight, - errorException = t.classPath - ) - } - } - ) -} - -internal fun BadQualityConfigSerializable.toDomain(): BadQualityConfig { - val latencyConfig = BadQualityConfig.LatencyConfig( - latencyTriggerProbability = latency.latencyTriggerProbability, - minLatencyMs = latency.minLatencyMs, - maxLatencyMs = latency.maxLatencyMs - ) - - val errorsList = errors.map { e -> - val type = if (!e.errorException.isNullOrEmpty()) { - BadQualityConfig.Error.Type.ErrorThrow(e.errorException) - } else { - BadQualityConfig.Error.Type.Body( - errorCode = e.errorCode ?: 0, - errorBody = e.errorBody.orEmpty(), - errorContentType = e.errorContentType.orEmpty() - ) - } - BadQualityConfig.Error( - weight = e.weight, - type = type - ) - } - - return BadQualityConfig( - latency = latencyConfig, - errorProbability = errorProbability, - errors = errorsList - ) -} \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt deleted file mode 100644 index 4fe64d4db..000000000 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/FloconNetworkRequestToJson.kt +++ /dev/null @@ -1,111 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package io.github.openflocon.flocon.network.core.noop.mapper - -import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.network.core.model.FloconNetworkCallRequest -import io.github.openflocon.flocon.network.core.model.FloconNetworkCallResponse -import io.github.openflocon.flocon.network.core.model.FloconWebSocketEvent -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid - -@Serializable -internal class FloconNetworkCallRequestRemote( - val floconCallId: String, - val floconNetworkType: String, - val isMocked: Boolean, - - val url: String, - val method: String, - val startTime: Long, - val requestBody: String?, - val requestHeaders: Map, - val requestSize: Long?, -) - -internal fun FloconNetworkCallRequest.floconNetworkCallRequestToJson(): String { - val remoteModel = FloconNetworkCallRequestRemote( - floconCallId = floconCallId, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - url = request.url, - method = request.method, - startTime = request.startTime, - requestBody = request.body, - requestHeaders = request.headers, - requestSize = request.size - ) - return FloconEncoder.json.encodeToString(remoteModel) -} - -@Serializable -internal class FloconNetworkCallResponseRemote( - val floconCallId: String, - val durationMs: Double, - val floconNetworkType: String, - val isMocked: Boolean, - val responseHttpCode: Int?, - val responseGrpcStatus: String?, - val responseContentType: String?, - val responseBody: String?, - val responseSize: Long?, - val responseHeaders: Map, - val requestHeaders: Map?, // we might receive the request headers later if the interceptor is at first position in the http interceptor chain - val responseError: String?, - val isImage: Boolean, -) - -internal fun FloconNetworkCallResponse.floconNetworkCallResponseToJson(): String { - val remoteModel = FloconNetworkCallResponseRemote( - floconCallId = floconCallId, - floconNetworkType = floconNetworkType, - isMocked = isMocked, - durationMs = durationMs, - responseHttpCode = response.httpCode, - responseGrpcStatus = response.grpcStatus, - responseContentType = response.contentType, - responseBody = response.body, - responseHeaders = response.headers, - requestHeaders = response.requestHeaders?.takeIf { - it.isNotEmpty() - }, - responseSize = response.size, - isImage = response.isImage, - responseError = response.error, - ) - - return FloconEncoder.json.encodeToString(remoteModel) -} - -@Serializable -internal class FloconWebSocketEventRemote( - val id: String, - val event: String, - val url: String, - val size: Long, - val timestamp: Long, - val message: String?, - val error: String?, -) - -internal fun FloconWebSocketEvent.floconNetworkWebSocketEventToJson(): String { - val remoteModel = FloconWebSocketEventRemote( - id = Uuid.random().toString(), - event = when (event) { - FloconWebSocketEvent.Event.Closed -> "closed" - FloconWebSocketEvent.Event.Closing -> "closing" - FloconWebSocketEvent.Event.Error -> "error" - FloconWebSocketEvent.Event.ReceiveMessage -> "received" - FloconWebSocketEvent.Event.SendMessage -> "sent" - FloconWebSocketEvent.Event.Open -> "open" - }, - url = websocketUrl, - size = size, - timestamp = timeStamp, - message = message, - error = error?.message - ) - return FloconEncoder.json.encodeToString(remoteModel) -} \ No newline at end of file diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt deleted file mode 100644 index 2f96ac6ab..000000000 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt +++ /dev/null @@ -1,123 +0,0 @@ -<<<<<<<< HEAD:FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/MockResponseToJson.kt -package io.github.openflocon.flocon.network.core.mapper -======== -package io.github.openflocon.flocon.network.core.noop.mapper ->>>>>>>> 2.0.0:FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/MockResponseToJson.kt - -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder -import io.github.openflocon.flocon.network.core.model.MockNetworkResponse -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString - -@Serializable -internal class MockNetworkResponseDataModel( - val expectation: Expectation, - val response: Response, -) { - @Serializable - class Expectation( - val urlPattern: String, // a regex - val method: String, // can be get, post, put, ... or a wildcard * - ) - - @Serializable - class Response( - val httpCode: Int?, - val body: String?, - val mediaType: String?, - val delay: Long?, - val headers: Map?, - val errorException: String?, - ) -} - - -internal fun parseMockResponses(jsonString: String): List { - try { - val remote = - FloconEncoder.json.decodeFromString>(jsonString) - return remote.mapNotNull { - it.toDomain() - } - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "mock network parsing issue", t) - return emptyList() - } -} - -internal fun MockNetworkResponseDataModel.toDomain(): MockNetworkResponse? { - return MockNetworkResponse( - expectation = MockNetworkResponse.Expectation( - urlPattern = expectation.urlPattern, - method = expectation.method, - ), - response = this.mapResponseToDomain() ?: return null - ) -} - -private fun MockNetworkResponseDataModel.mapResponseToDomain(): MockNetworkResponse.Response? { - return response.run { - when { - errorException != null -> MockNetworkResponse.Response.ErrorThrow( - classPath = errorException, - delay = delay ?: 0L, - ) - - httpCode != null -> MockNetworkResponse.Response.Body( - httpCode = httpCode, - body = body ?: "", - delay = delay ?: 0L, - mediaType = mediaType ?: "", - headers = headers ?: emptyMap() - ) - - else -> run { - FloconLogger.logError("error parsing mock response", null) - return@run null - } - } - } -} - - -internal fun writeMockResponsesToJson(mocks: List): String { - return try { - FloconEncoder.json.encodeToString(mocks.map { it.toRemote() }) - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "mock network writing issue", t) - return "[]" - } -} - -private fun MockNetworkResponse.toRemote(): MockNetworkResponseDataModel { - return MockNetworkResponseDataModel( - expectation = MockNetworkResponseDataModel.Expectation( - urlPattern = expectation.urlPattern, - method = expectation.method, - ), - response = mapResponseToRemote(), - ) -} - -private fun MockNetworkResponse.mapResponseToRemote(): MockNetworkResponseDataModel.Response { - return when (val response = this.response) { - is MockNetworkResponse.Response.ErrorThrow -> MockNetworkResponseDataModel.Response( - errorException = response.classPath, - delay = response.delay, - body = null, - headers = null, - httpCode = null, - mediaType = null, - ) - - is MockNetworkResponse.Response.Body -> MockNetworkResponseDataModel.Response( - errorException = null, - delay = response.delay, - body = response.body, - headers = response.headers, - httpCode = response.httpCode, - mediaType = response.mediaType, - ) - } -} diff --git a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt b/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt deleted file mode 100644 index 567a072cf..000000000 --- a/FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt +++ /dev/null @@ -1,29 +0,0 @@ -<<<<<<<< HEAD:FloconAndroid/network/core/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/mapper/Websocket.kt -package io.github.openflocon.flocon.network.core.mapper -======== -package io.github.openflocon.flocon.network.core.noop.mapper ->>>>>>>> 2.0.0:FloconAndroid/network/core-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/network/core/noop/mapper/Websocket.kt - -import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.core.FloconEncoder -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString - -@Serializable -internal class WebSocketMockMessage( - val id: String, - val message: String, -) - -internal fun webSocketIdsToJsonArray(ids: Collection): String { - return FloconEncoder.json.encodeToString(ids) -} - -internal fun parseWebSocketMockMessage(jsonString: String): WebSocketMockMessage? { - try { - return FloconEncoder.json.decodeFromString(jsonString) - } catch (t: Throwable) { - FloconLogger.logError(t.message ?: "mock wesocket network parsing issue", t) - } - return null -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index 91a7f5647..545be5263 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -94,6 +94,9 @@ dependencies { debugImplementation(projects.analytics) releaseImplementation(projects.analyticsNoOp) + debugImplementation(projects.crashreporter) + releaseImplementation(projects.crashreporterNoOp) + debugImplementation(project(":database:room")) releaseImplementation(project(":database:room-no-op")) debugImplementation(project(":database:room3")) diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index e9b0f8524..c4781ac24 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -43,6 +43,12 @@ include(":analytics") include(":analytics-no-op") include(":crashreporter") include(":crashreporter-no-op") +include(":device") +include(":device-no-op") +include(":files") +include(":files-no-op") +include(":sharedprefs") +include(":sharedprefs-no-op") include(":network:core") include(":network:core-no-op") include(":database:core") diff --git a/FloconAndroid/sharedprefs-no-op/build.gradle.kts b/FloconAndroid/sharedprefs-no-op/build.gradle.kts new file mode 100644 index 000000000..9de465592 --- /dev/null +++ b/FloconAndroid/sharedprefs-no-op/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("flocon.kotlin.multiplatform") + id("flocon.publish") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.coroutines.core) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.sharedprefs.noop" +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-sharedprefs-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt new file mode 100644 index 000000000..05e27c6c0 --- /dev/null +++ b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -0,0 +1,48 @@ +package io.github.openflocon.flocon.plugins.sharedprefs + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconSharedPreferenceModel + +class FloconPreferencesConfig : FloconPluginConfig + +interface FloconPreferencesPlugin : FloconPlugin { + fun register(sharedPreference: FloconSharedPreferenceModel) +} + +object FloconPreferences : FloconPluginFactory { + override val name: String = "Preferences" + override val pluginId: String = Protocol.ToDevice.SharedPreferences.Plugin + override fun createConfig(context: FloconContext) = FloconPreferencesConfig() + override fun install( + pluginConfig: FloconPreferencesConfig, + floconConfig: FloconConfig, + encoder: FloconEncoder + ): FloconPreferencesPlugin { + return FloconSharedPrefsPluginNoOpImpl() + } +} + +internal class FloconSharedPrefsPluginNoOpImpl : FloconPlugin, FloconPreferencesPlugin { + override val key: String = "SHARED_PREF" + + override suspend fun onMessageReceived( + method: String, + body: String, + ) { + // no op + } + + override suspend fun onConnectedToServer() { + // no op + } + + override fun register(sharedPreference: FloconSharedPreferenceModel) { + // no op + } +} diff --git a/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt new file mode 100644 index 000000000..a0b612b92 --- /dev/null +++ b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt @@ -0,0 +1,4 @@ +package io.github.openflocon.flocon.plugins.sharedprefs.model + +class FloconSharedPreferenceModel { +} diff --git a/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt new file mode 100644 index 000000000..4ff1e05f8 --- /dev/null +++ b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt @@ -0,0 +1,50 @@ +package io.github.openflocon.flocon.pluginsold.sharedprefs + +import io.github.openflocon.flocon.FloconConfig +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconPlugin +import io.github.openflocon.flocon.FloconPluginConfig +import io.github.openflocon.flocon.FloconPluginFactory +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.pluginsold.sharedprefs.model.FloconSharedPreferenceModel + +class FloconPreferencesConfig : FloconPluginConfig + +object FloconPreferences : FloconPluginFactory { + override val name: String = "Preferences" + override val pluginId: String = "preferences" + override fun createConfig(context: FloconContext): FloconPreferencesConfig { + return FloconPreferencesConfig() + } + + override fun install( + pluginConfig: FloconPreferencesConfig, + floconConfig: FloconConfig, + encoder: FloconEncoder + ): FloconPreferencesPlugin { + return FloconSharedPrefsPluginNoOpImpl() + } +} + +interface FloconPreferencesPlugin : FloconPlugin { + fun register(sharedPreference: FloconSharedPreferenceModel) +} + +internal class FloconSharedPrefsPluginNoOpImpl : FloconPlugin, FloconPreferencesPlugin { + override val key: String = "SHARED_PREF" + + override suspend fun onMessageReceived( + method: String, + body: String, + ) { + // no op + } + + override suspend fun onConnectedToServer() { + // no op + } + + override fun register(sharedPreference: FloconSharedPreferenceModel) { + // no op + } +} diff --git a/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt new file mode 100644 index 000000000..86fb0e0b6 --- /dev/null +++ b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt @@ -0,0 +1,4 @@ +package io.github.openflocon.flocon.pluginsold.sharedprefs.model + +class FloconSharedPreferenceModel { +} diff --git a/FloconAndroid/sharedprefs/build.gradle.kts b/FloconAndroid/sharedprefs/build.gradle.kts new file mode 100644 index 000000000..9cee3f151 --- /dev/null +++ b/FloconAndroid/sharedprefs/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("flocon.kotlin.multiplatform") + id("flocon.publish") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":flocon")) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + } + } +} + +android { + namespace = "io.github.openflocon.flocon.sharedprefs" +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-sharedprefs", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) +} diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt b/FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt similarity index 100% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt rename to FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.android.kt diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPreference.kt b/FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPreference.kt similarity index 100% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPreference.kt rename to FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPreference.kt diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt b/FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt similarity index 100% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt rename to FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.android.kt diff --git a/FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/SharedPreferencesFinder.kt b/FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/SharedPreferencesFinder.kt similarity index 100% rename from FloconAndroid/flocon/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/SharedPreferencesFinder.kt rename to FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/SharedPreferencesFinder.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreferenceWrapper.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconSharedPreferenceModel.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/PreferenceRowDataModel.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/PreferenceRowDataModel.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/PreferenceRowDataModel.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/PreferenceRowDataModel.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/fromdevice/SharedPreferenceValueResultDataModel.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceEditSharedPreferenceValueMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPreferenceValueMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceGetSharedPrefsMessage.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/FloconSharedPrefsPlugin.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/buildFloconPreferencesDataSource.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconPreference.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconPreference.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconPreference.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconPreference.kt diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt similarity index 100% rename from FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt rename to FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/pluginsold/sharedprefs/model/FloconSharedPreferenceModel.kt diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt b/FloconAndroid/sharedprefs/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt similarity index 100% rename from FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt rename to FloconAndroid/sharedprefs/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt b/FloconAndroid/sharedprefs/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt similarity index 100% rename from FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt rename to FloconAndroid/sharedprefs/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.jvm.kt diff --git a/FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt b/FloconAndroid/sharedprefs/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt similarity index 100% rename from FloconAndroid/flocon/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt rename to FloconAndroid/sharedprefs/src/wasmJsMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.wasmJs.kt diff --git a/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesNoOp.kt b/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesNoOp.kt index 49946f2fc..53f21521d 100644 --- a/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesNoOp.kt +++ b/FloconAndroid/tables-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/tables/FloconTablesNoOp.kt @@ -6,6 +6,7 @@ import io.github.openflocon.flocon.FloconPlugin import io.github.openflocon.flocon.FloconPluginConfig import io.github.openflocon.flocon.FloconPluginFactory import io.github.openflocon.flocon.Protocol +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.plugins.tables.model.TableItem interface FloconTablePlugin : FloconPlugin { @@ -20,7 +21,8 @@ object FloconTable : FloconPluginFactory { override fun createConfig(context: FloconContext) = FloconTableConfig() override fun install( pluginConfig: FloconTableConfig, - floconConfig: FloconConfig + floconConfig: FloconConfig, + encoder: FloconEncoder ): FloconTablePlugin { return FloconTablePluginNoOp } From 2149379b8289049bdbbbaaeb314ec7c78dd52763 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Fri, 29 May 2026 10:56:38 +0200 Subject: [PATCH 35/38] feat: FloconSpacing --- .../app/ui/settings/SettingsScreen.kt | 16 ++++++------ .../ui/view/leftpannel/LeftPannelDivider.kt | 4 ++- .../app/ui/view/leftpannel/LeftPannelView.kt | 6 ++--- .../app/ui/view/leftpannel/PannelLabel.kt | 2 +- .../app/ui/view/leftpannel/PannelView.kt | 8 +++--- .../app/ui/view/topbar/MainScreenTopBar.kt | 4 +-- .../ui/view/topbar/TopBarDeviceAndAppView.kt | 3 ++- .../ui/view/topbar/actions/TopBarActions.kt | 3 ++- .../app/ui/view/topbar/app/TopBarAppView.kt | 4 +-- .../ui/view/topbar/device/TopBarDeviceView.kt | 4 +-- .../features/database/view/DatabaseScreen.kt | 5 ++-- .../view/databases_tables/DatabaseItemView.kt | 4 +-- .../DatabasesAndTablesView.kt | 12 ++++----- .../view/databases_tables/TableItemView.kt | 6 ++--- .../mock/edition/view/MockNetworkLabelView.kt | 2 +- .../edition/view/MockNetworkMethodDropdown.kt | 5 ++-- .../mock/edition/view/NetworkEditionWindow.kt | 26 +++++++++---------- .../network/mock/list/view/MockLineView.kt | 8 +++--- .../library/designsystem/FloconTheme.kt | 7 +++++ .../designsystem/theme/FloconSpacing.kt | 21 +++++++++++++++ 20 files changed, 92 insertions(+), 58 deletions(-) create mode 100644 FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/theme/FloconSpacing.kt diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt index 115d0c254..ed54cbc7e 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt @@ -87,12 +87,12 @@ private fun SettingsScreen( initialValue = true ) { Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), modifier = Modifier - .padding(8.dp) + .padding(FloconTheme.spacing.small) .clip(FloconTheme.shapes.medium) .background(FloconTheme.colorPalette.primary) - .padding(all = 8.dp) + .padding(all = FloconTheme.spacing.small) ) { if (needsAdbSetup) { Text( @@ -103,7 +103,7 @@ private fun SettingsScreen( } else { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.extraSmall) ) { FloconIcon( imageVector = Icons.Outlined.Check, @@ -125,7 +125,7 @@ private fun SettingsScreen( modifier = Modifier.fillMaxWidth() ) Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small) ) { SettingsButton( text = stringResource(Res.string.general_save), @@ -144,10 +144,10 @@ private fun SettingsScreen( ) { Column( modifier = Modifier - .padding(8.dp) + .padding(FloconTheme.spacing.small) .clip(FloconTheme.shapes.medium) .background(FloconTheme.colorPalette.primary) - .padding(all = 8.dp) + .padding(all = FloconTheme.spacing.small) ) { FloconSlider( value = uiState.fontSizeMultiplier, @@ -164,7 +164,7 @@ private fun SettingsScreen( SettingsButton( onClick = { showLicenses = true }, text = stringResource(Res.string.settings_licenses), - modifier = Modifier.padding(8.dp) + modifier = Modifier.padding(FloconTheme.spacing.small) ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/LeftPannelDivider.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/LeftPannelDivider.kt index 94aa59acc..deacee2f8 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/LeftPannelDivider.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/LeftPannelDivider.kt @@ -7,10 +7,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import io.github.openflocon.library.designsystem.FloconTheme + @Composable fun LeftPannelDivider(modifier: Modifier = Modifier) { HorizontalDivider( - modifier = modifier.padding(horizontal = 4.dp), + modifier = modifier.padding(horizontal = FloconTheme.spacing.extraSmall), thickness = 1.dp, color = Color.Gray, // TODO Change ) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/LeftPannelView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/LeftPannelView.kt index e798846bc..cae56e29a 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/LeftPannelView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/LeftPannelView.kt @@ -45,7 +45,7 @@ fun LeftPanelView( modifier = modifier .clip(FloconTheme.shapes.medium) .background(FloconTheme.colorPalette.primary) - .padding(8.dp) + .padding(FloconTheme.spacing.small) ) { MenuSection( current = current, @@ -53,7 +53,7 @@ fun LeftPanelView( expanded = expanded, onClickItem = onClickItem, ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(FloconTheme.spacing.medium)) Spacer(Modifier.weight(1f)) MenuItems( current = current, @@ -105,7 +105,7 @@ private fun ColumnScope.MenuItems( onClick = { onClickItem(item) }, ) if (index != items.lastIndex) - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(FloconTheme.spacing.extraSmall)) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/PannelLabel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/PannelLabel.kt index 0575fbb49..c3434236d 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/PannelLabel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/PannelLabel.kt @@ -35,7 +35,7 @@ fun PannelLabel( Text( modifier = Modifier .fillMaxWidth() - .padding(start = 12.dp, bottom = 4.dp), + .padding(start = FloconTheme.spacing.medium, bottom = FloconTheme.spacing.extraSmall), text = text, style = FloconTheme.typography.bodyLarge.copy( fontWeight = FontWeight.Thin, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/PannelView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/PannelView.kt index 5130743f0..3e31fb1f4 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/PannelView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/leftpannel/PannelView.kt @@ -72,7 +72,7 @@ fun PanelView( 0.3f } else 1f ) - val horizontalPadding = 12.dp + val horizontalPadding = FloconTheme.spacing.medium Row( modifier = modifier @@ -83,13 +83,13 @@ fun PanelView( alpha = lineAlpha } .clickable(onClick = onClick, interactionSource = interactionSource, indication = null) - .padding(horizontal = horizontalPadding, vertical = 4.dp), + .padding(horizontal = horizontalPadding, vertical = FloconTheme.spacing.extraSmall), verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier .size(PanelContentMinSize - horizontalPadding.times(2)) - .padding(4.dp), + .padding(FloconTheme.spacing.extraSmall), imageVector = icon, contentDescription = "Description de mon image", tint = iconColor, @@ -104,7 +104,7 @@ fun PanelView( color = FloconTheme.colorPalette.onSurface, style = FloconTheme.typography.bodyMedium, maxLines = 1, - modifier = Modifier.padding(start = 12.dp), + modifier = Modifier.padding(start = FloconTheme.spacing.medium), ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/MainScreenTopBar.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/MainScreenTopBar.kt index 4a5d43c46..fb938ae9b 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/MainScreenTopBar.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/MainScreenTopBar.kt @@ -45,7 +45,7 @@ fun MainScreenTopBar( Row( modifier = modifier .background(FloconTheme.colorPalette.surface) - .padding(vertical = 8.dp, horizontal = 12.dp), + .padding(vertical = FloconTheme.spacing.small, horizontal = FloconTheme.spacing.medium), verticalAlignment = Alignment.CenterVertically, ) { Title() @@ -75,7 +75,7 @@ private fun Title( ) { Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), verticalAlignment = Alignment.CenterVertically, ) { Image( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/TopBarDeviceAndAppView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/TopBarDeviceAndAppView.kt index 5bcaa0bf0..c4dfc316a 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/TopBarDeviceAndAppView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/TopBarDeviceAndAppView.kt @@ -17,6 +17,7 @@ import io.github.openflocon.flocondesktop.app.ui.model.DeviceItemUiModel import io.github.openflocon.flocondesktop.app.ui.model.DevicesStateUiModel import io.github.openflocon.flocondesktop.app.ui.view.topbar.app.TopBarAppDropdown import io.github.openflocon.flocondesktop.app.ui.view.topbar.device.TopBarDeviceDropdown +import io.github.openflocon.library.designsystem.FloconTheme @Composable internal fun TopBarDeviceAndAppView( @@ -30,7 +31,7 @@ internal fun TopBarDeviceAndAppView( ) { Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), verticalAlignment = Alignment.CenterVertically, ) { TopBarDeviceDropdown( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/actions/TopBarActions.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/actions/TopBarActions.kt index aff056108..50a4430fb 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/actions/TopBarActions.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/actions/TopBarActions.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.github.openflocon.flocondesktop.app.ui.model.DevicesStateUiModel import io.github.openflocon.flocondesktop.app.ui.model.RecordVideoStateUiModel +import io.github.openflocon.library.designsystem.FloconTheme @Composable internal fun TopBarActions( @@ -26,7 +27,7 @@ internal fun TopBarActions( Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.extraSmall) ) { TopBarButton( active = false, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/app/TopBarAppView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/app/TopBarAppView.kt index 6c9f5eb22..0dc599573 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/app/TopBarAppView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/app/TopBarAppView.kt @@ -46,8 +46,8 @@ internal fun TopBarAppView( deleteClick: (() -> Unit)? = null, ) { Row( - modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.padding(horizontal = FloconTheme.spacing.small, vertical = FloconTheme.spacing.extraSmall), + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), verticalAlignment = Alignment.CenterVertically, ) { AppImage( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/device/TopBarDeviceView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/device/TopBarDeviceView.kt index 0af13fe71..c1aff68c6 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/device/TopBarDeviceView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/device/TopBarDeviceView.kt @@ -51,8 +51,8 @@ internal fun TopBarDeviceView( deleteClick: (() -> Unit)? = null, ) { Row( - modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.padding(horizontal = FloconTheme.spacing.small, vertical = FloconTheme.spacing.extraSmall), + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), verticalAlignment = Alignment.CenterVertically, ) { Box( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/DatabaseScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/DatabaseScreen.kt index 4fb5cadc1..a502d248f 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/DatabaseScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/DatabaseScreen.kt @@ -21,6 +21,7 @@ import io.github.openflocon.flocondesktop.features.database.model.DatabaseScreen import io.github.openflocon.flocondesktop.features.database.model.DatabaseTabState import io.github.openflocon.flocondesktop.features.database.model.DatabasesStateUiModel import io.github.openflocon.flocondesktop.features.database.view.databases_tables.DatabasesAndTablesView +import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.library.designsystem.components.FloconFeature import org.koin.compose.viewmodel.koinViewModel @@ -70,11 +71,11 @@ fun DatabaseScreen( favorites = favorites, onAction = onAction ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(FloconTheme.spacing.medium)) Column(modifier = Modifier.fillMaxWidth()) { DatabaseTabsView( - modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = FloconTheme.spacing.medium), tabs = tabs, selected = selectedTab, onAction = { diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/DatabaseItemView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/DatabaseItemView.kt index e835b9b0b..270602c02 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/DatabaseItemView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/DatabaseItemView.kt @@ -113,7 +113,7 @@ private fun DatabaseView( }, onDoubleClick = { onDatabaseDoubleClicked(state) } - ).padding(horizontal = 12.dp, vertical = 8.dp), + ).padding(horizontal = FloconTheme.spacing.medium, vertical = FloconTheme.spacing.small), verticalAlignment = Alignment.CenterVertically, ) { Image( @@ -122,7 +122,7 @@ private fun DatabaseView( contentDescription = null, colorFilter = ColorFilter.tint(textColor), ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(FloconTheme.spacing.extraSmall)) Text( state.name, color = textColor, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/DatabasesAndTablesView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/DatabasesAndTablesView.kt index dbbda17a4..13d9a7a6b 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/DatabasesAndTablesView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/DatabasesAndTablesView.kt @@ -51,8 +51,8 @@ fun DatabasesAndTablesView( Modifier.fillMaxWidth() .weight(1f) .verticalScroll(rememberScrollState()) - .padding(all = 4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) + .padding(all = FloconTheme.spacing.extraSmall), + verticalArrangement = Arrangement.spacedBy(FloconTheme.spacing.extraSmall) ) { Text( "Databases", @@ -62,7 +62,7 @@ fun DatabasesAndTablesView( ), maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp) + modifier = Modifier.padding(horizontal = FloconTheme.spacing.medium, vertical = FloconTheme.spacing.small) ) when (state) { DatabasesStateUiModel.Empty -> Unit @@ -104,8 +104,8 @@ fun DatabasesAndTablesView( Modifier.fillMaxWidth() .weight(0.4f) .verticalScroll(rememberScrollState()) - .padding(all = 4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) + .padding(all = FloconTheme.spacing.extraSmall), + verticalArrangement = Arrangement.spacedBy(FloconTheme.spacing.extraSmall) ) { Text( stringResource(Res.string.databases_favorites), @@ -115,7 +115,7 @@ fun DatabasesAndTablesView( ), maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp) + modifier = Modifier.padding(horizontal = FloconTheme.spacing.medium, vertical = FloconTheme.spacing.small) ) favorites.forEach { diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/TableItemView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/TableItemView.kt index f4fa41006..b7f65b910 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/TableItemView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/database/view/databases_tables/TableItemView.kt @@ -99,10 +99,10 @@ private fun TableView( onTableDoubleClicked(item) } ) - .padding(horizontal = 12.dp) - .padding(vertical = 4.dp), + .padding(horizontal = FloconTheme.spacing.medium) + .padding(vertical = FloconTheme.spacing.extraSmall), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.extraSmall), ) { val color = FloconTheme.colorPalette.onSurface Image( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/MockNetworkLabelView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/MockNetworkLabelView.kt index 90aafc693..5ec8bbc32 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/MockNetworkLabelView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/MockNetworkLabelView.kt @@ -12,7 +12,7 @@ import io.github.openflocon.library.designsystem.FloconTheme internal fun MockNetworkLabelView(label: String) { Text( label, - modifier = Modifier.padding(start = 4.dp), + modifier = Modifier.padding(start = FloconTheme.spacing.extraSmall), color = FloconTheme.colorPalette.onSurface, style = FloconTheme.typography.bodyMedium.copy( fontWeight = FontWeight.Thin, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/MockNetworkMethodDropdown.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/MockNetworkMethodDropdown.kt index c51909a30..4b9b1cea5 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/MockNetworkMethodDropdown.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/MockNetworkMethodDropdown.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.github.openflocon.flocondesktop.features.network.mock.edition.model.MockNetworkMethodUi +import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.library.designsystem.components.FloconDropdownMenu @Composable @@ -23,7 +24,7 @@ fun MockNetworkMethodDropdown( ) { var expanded by remember { mutableStateOf(false) } - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(FloconTheme.spacing.extraSmall)) { MockNetworkLabelView(label) Box { @@ -39,7 +40,7 @@ fun MockNetworkMethodDropdown( ) { MockNetworkMethodUi.entries.forEach { method -> MockNetworkMethodView( - modifier = Modifier.padding(all = 4.dp), + modifier = Modifier.padding(all = FloconTheme.spacing.extraSmall), method = method, onClick = { onValueChange(method) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/NetworkEditionWindow.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/NetworkEditionWindow.kt index ef01e24f6..887c5413f 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/NetworkEditionWindow.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/edition/view/NetworkEditionWindow.kt @@ -117,7 +117,7 @@ fun MockEditorScreen( Column( modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), ) { FloconDialogHeader( modifier = Modifier @@ -127,7 +127,7 @@ fun MockEditorScreen( error?.let { Text( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = FloconTheme.spacing.large), text = it, color = MaterialTheme.colorScheme.error, ) } @@ -140,8 +140,8 @@ fun MockEditorScreen( Column( modifier = Modifier .weight(2f) - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + .padding(horizontal = FloconTheme.spacing.large, vertical = FloconTheme.spacing.small), + verticalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), ) { // Section Expectation Text( @@ -182,8 +182,8 @@ fun MockEditorScreen( Column( modifier = Modifier .weight(3f) - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + .padding(horizontal = FloconTheme.spacing.large, vertical = FloconTheme.spacing.small), + verticalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), ) { // Section Response Text( @@ -287,7 +287,7 @@ fun MockEditorScreen( FlowRow( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), ) { NetworkMockMediaType( text = "application/json", @@ -424,7 +424,7 @@ fun MockEditorScreen( color = FloconTheme.colorPalette.primary, shape = FloconTheme.shapes.medium, ) - .padding(vertical = 4.dp, horizontal = 8.dp), + .padding(vertical = FloconTheme.spacing.extraSmall, horizontal = FloconTheme.spacing.small), ) { val throwable = jsonError if(throwable == null) { @@ -447,8 +447,8 @@ fun MockEditorScreen( } } Row( - modifier = Modifier.fillMaxWidth().padding(all = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().padding(all = FloconTheme.spacing.small), + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), verticalAlignment = Alignment.CenterVertically ) { FloconCheckbox( @@ -489,7 +489,7 @@ fun NetworkMockMediaType(text: String, onClicked: (text: String) -> Unit) { .clickable { onClicked(text) } - .padding(horizontal = 12.dp, vertical = 2.dp), + .padding(horizontal = FloconTheme.spacing.medium, vertical = 2.dp), ) } @@ -504,7 +504,7 @@ private fun HeaderInputField( Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small), ) { FloconTextField( value = key, @@ -527,7 +527,7 @@ private fun HeaderInputField( .clip(RoundedCornerShape(2.dp)) .clickable { onRemove() - }.padding(all = 4.dp), + }.padding(all = FloconTheme.spacing.extraSmall), contentAlignment = Alignment.Center, ) { Image( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/list/view/MockLineView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/list/view/MockLineView.kt index da97c77c5..137c53ba3 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/list/view/MockLineView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/mock/list/view/MockLineView.kt @@ -40,12 +40,12 @@ fun MockLineView( Row( modifier = modifier .clickable { onClicked(item.id) } - .padding(vertical = 8.dp), + .padding(vertical = FloconTheme.spacing.small), verticalAlignment = Alignment.CenterVertically, ) { Box( modifier = Modifier - .padding(horizontal = 8.dp) + .padding(horizontal = FloconTheme.spacing.small) .width(50.dp) .height(12.dp), contentAlignment = Alignment.Center, @@ -83,7 +83,7 @@ fun MockLineView( style = FloconTheme.typography.titleSmall, color = FloconTheme.colorPalette.onSurface, ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(FloconTheme.spacing.small)) Text( text = item.urlPattern, maxLines = 1, @@ -96,7 +96,7 @@ fun MockLineView( Row( modifier = Modifier - .padding(horizontal = 8.dp) + .padding(horizontal = FloconTheme.spacing.small) .height(12.dp) ) { FloconCheckbox( diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/FloconTheme.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/FloconTheme.kt index c61554029..0ce18208b 100644 --- a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/FloconTheme.kt +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/FloconTheme.kt @@ -23,8 +23,10 @@ import androidx.compose.ui.unit.sp import io.github.openflocon.library.designsystem.components.FloconMenuRepresentation import io.github.openflocon.library.designsystem.theme.FloconColorPaletteNew import io.github.openflocon.library.designsystem.theme.FloconShape +import io.github.openflocon.library.designsystem.theme.FloconSpacing import io.github.openflocon.library.designsystem.theme.LocalFloconColorPalette import io.github.openflocon.library.designsystem.theme.LocalFloconShape +import io.github.openflocon.library.designsystem.theme.LocalFloconSpacing import io.github.openflocon.library.designsystem.theme.darkPalette import io.github.openflocon.library.designsystem.theme.lightPalette import io.github.openflocon.library.designsystem.theme.materialDarkScheme @@ -44,6 +46,10 @@ object FloconTheme { val shapes: FloconShape @Composable @ReadOnlyComposable get() = LocalFloconShape.current + + val spacing: FloconSpacing + @Composable @ReadOnlyComposable + get() = LocalFloconSpacing.current } @Composable @@ -92,6 +98,7 @@ fun FloconTheme( CompositionLocalProvider( LocalIndication provides ripple, LocalFloconColorPalette provides colorPalette, + LocalFloconSpacing provides FloconSpacing(), LocalTextSelectionColors provides selectionTextColor, LocalScrollbarStyle provides scrollbarStyle, LocalMinimumInteractiveComponentSize provides Dp.Unspecified, diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/theme/FloconSpacing.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/theme/FloconSpacing.kt new file mode 100644 index 000000000..12cc58455 --- /dev/null +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/theme/FloconSpacing.kt @@ -0,0 +1,21 @@ +package io.github.openflocon.library.designsystem.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Immutable +data class FloconSpacing( + val none: Dp = 0.dp, + val extraSmall: Dp = 4.dp, + val small: Dp = 8.dp, + val medium: Dp = 12.dp, + val large: Dp = 16.dp, + val extraLarge: Dp = 24.dp, + val doubleExtraLarge: Dp = 32.dp, + val tripleExtraLarge: Dp = 48.dp, + val huge: Dp = 64.dp +) + +val LocalFloconSpacing = staticCompositionLocalOf { FloconSpacing() } From ce6a978200c8a7ff063f9980c3ed26ab92387c88 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Sun, 28 Jun 2026 12:04:09 +0200 Subject: [PATCH 36/38] feat: Migrate JDK --- .../gradle/gradle-daemon-jvm.properties | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/FloconAndroid/gradle/gradle-daemon-jvm.properties b/FloconAndroid/gradle/gradle-daemon-jvm.properties index cfd299ab0..baa28d154 100644 --- a/FloconAndroid/gradle/gradle-daemon-jvm.properties +++ b/FloconAndroid/gradle/gradle-daemon-jvm.properties @@ -1,13 +1,13 @@ #This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/584d2f01a3c6e59ebb9478a182f5f714/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/7352c4c0c11b2db21fdd7541204de287/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/584d2f01a3c6e59ebb9478a182f5f714/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/7352c4c0c11b2db21fdd7541204de287/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/12ed6bcbab330f7afa37d16220b272a3/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/a7e1d8e6e800a81047d4aec26156ef5c/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/584d2f01a3c6e59ebb9478a182f5f714/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/7352c4c0c11b2db21fdd7541204de287/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/3676ee7aa5095d7f22645eb0f22ca159/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/fc98f2d434c5796fe6ec02f0f22957b3/redirect -toolchainVendor=AZUL -toolchainVersion=17 +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 From 3a92ac9d03dfe290ff385eba9fc36a3ca1f9834f Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Sun, 28 Jun 2026 12:38:43 +0200 Subject: [PATCH 37/38] feat: Remove android sample --- FloconAndroid/database/room/build.gradle.kts | 2 +- .../FloconRoomDatabaseProviderImpl.ios.kt | 20 ++ .../FloconRoomDatabaseProviderImpl.jvm.kt | 20 ++ .../database/FloconDatabasePlugin.ios.kt | 127 --------- .../database/FloconDatabasePlugin.jvm.kt | 129 --------- .../flocon/websocket/FloconHttpClient.jvm.kt | 1 + .../datasource/FloconNetworkDataSource.ios.kt | 6 +- .../datasource/FloconNetworkDataSourceImpl.kt | 5 +- .../datasource/FloconNetworkDataSource.jvm.kt | 6 +- .../datasource/FloconNetworkDataSourceImpl.kt | 16 +- FloconAndroid/sample-android-only/.gitignore | 1 - .../sample-android-only/build.gradle.kts | 218 ---------------- .../sample-android-only/proguard-rules.pro | 21 -- .../src/main/AndroidManifest.xml | 46 ---- .../src/main/ic_launcher-playstore.png | Bin 292223 -> 0 bytes .../myapplication/DummyHttpKtorCaller.kt | 66 ----- .../flocon/myapplication/MainActivity.kt | 247 ------------------ .../dashboard/InitializeDashboard.kt | 194 -------------- .../myapplication/dashboard/device/Device.kt | 39 --- .../myapplication/dashboard/tokens/Tokens.kt | 17 -- .../myapplication/dashboard/user/User.kt | 17 -- .../myapplication/database/DogDatabase.kt | 45 ---- .../myapplication/database/FoodDatabase.kt | 34 --- .../database/InitializeDatabases.kt | 146 ----------- .../myapplication/database/dao/DogDao.kt | 26 -- .../myapplication/database/dao/FoodDao.kt | 18 -- .../myapplication/database/model/DogEntity.kt | 15 -- .../database/model/FoodEntity.kt | 13 - .../database/model/HumanEntity.kt | 11 - .../database/model/HumanWithDogEntity.kt | 26 -- .../sharedpreferences/SharedPreferences.kt | 70 ----- .../myapplication/table/InitializeTable.kt | 13 - .../flocon/myapplication/ui/ImagesListView.kt | 35 --- .../flocon/myapplication/ui/theme/Color.kt | 11 - .../flocon/myapplication/ui/theme/Theme.kt | 57 ---- .../flocon/myapplication/ui/theme/Type.kt | 34 --- .../res/drawable/ic_launcher_background.xml | 74 ------ .../res/drawable/ic_launcher_foreground.xml | 30 --- .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 - .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 5418 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 13956 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 7736 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 3256 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 7706 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 4358 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 8584 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 21662 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 11790 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 15670 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 42028 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 20670 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 23736 -> 0 bytes .../ic_launcher_foreground.webp | Bin 70566 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 31744 -> 0 bytes .../src/main/res/values/colors.xml | 10 - .../src/main/res/values/strings.xml | 3 - .../src/main/res/values/themes.xml | 5 - .../src/main/res/xml/backup_rules.xml | 13 - .../main/res/xml/data_extraction_rules.xml | 19 -- .../sample-multiplatform/build.gradle.kts | 118 ++++++++- .../release.jks | Bin .../androidMain}/graphql/GetUserInfo.graphql | 2 +- .../graphql/GetUserRepositories.graphql | 2 +- .../src/androidMain}/graphql/schema.json | 0 .../flocon/myapplication/multi/Databases.kt | 113 ++++---- .../myapplication/multi}/DummyHttpCaller.kt | 4 +- .../multi}/DummyWebsocketCaller.kt | 2 +- .../myapplication/multi/MainActivity.kt | 71 ++++- .../multi}/graphql/GraphQlTester.kt | 9 +- .../multi}/grpc/InitializeGrpc.kt | 4 +- .../multi}/images/InitializeImages.kt | 4 +- .../multi}/sharedpreferences/Datastores.kt | 2 +- .../multi/ui/PlatformSpecificTests.android.kt | 123 +++++++++ .../src/androidMain}/proto/helloworld.proto | 0 .../flocon/myapplication/multi/ui/App.kt | 6 + .../flocon/myapplication/multi/Main.kt | 47 ++-- .../multi/ui/PlatformSpecificTests.desktop.kt | 9 + .../com/florent37/myapplication/Databases.kt | 12 +- .../multi/ui/PlatformSpecificTests.ios.kt | 9 + FloconAndroid/settings.gradle.kts | 1 - 81 files changed, 505 insertions(+), 1951 deletions(-) create mode 100644 FloconAndroid/database/room/src/iosMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.ios.kt create mode 100644 FloconAndroid/database/room/src/jvmMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.jvm.kt delete mode 100644 FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt delete mode 100644 FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.jvm.kt delete mode 100644 FloconAndroid/sample-android-only/.gitignore delete mode 100644 FloconAndroid/sample-android-only/build.gradle.kts delete mode 100644 FloconAndroid/sample-android-only/proguard-rules.pro delete mode 100644 FloconAndroid/sample-android-only/src/main/AndroidManifest.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/ic_launcher-playstore.png delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/DummyHttpKtorCaller.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/device/Device.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/tokens/Tokens.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/user/User.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/FoodDatabase.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/dao/DogDao.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/dao/FoodDao.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/DogEntity.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/FoodEntity.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/HumanEntity.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/HumanWithDogEntity.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/ImagesListView.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Color.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Theme.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Type.kt delete mode 100644 FloconAndroid/sample-android-only/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/res/drawable/ic_launcher_foreground.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp delete mode 100644 FloconAndroid/sample-android-only/src/main/res/values/colors.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/res/values/strings.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/res/values/themes.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/res/xml/backup_rules.xml delete mode 100644 FloconAndroid/sample-android-only/src/main/res/xml/data_extraction_rules.xml rename FloconAndroid/{sample-android-only => sample-multiplatform}/release.jks (100%) rename FloconAndroid/{sample-android-only/src/main => sample-multiplatform/src/androidMain}/graphql/GetUserInfo.graphql (98%) rename FloconAndroid/{sample-android-only/src/main => sample-multiplatform/src/androidMain}/graphql/GetUserRepositories.graphql (99%) rename FloconAndroid/{sample-android-only/src/main => sample-multiplatform/src/androidMain}/graphql/schema.json (100%) rename FloconAndroid/{sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication => sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi}/DummyHttpCaller.kt (97%) rename FloconAndroid/{sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication => sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi}/DummyWebsocketCaller.kt (97%) rename FloconAndroid/{sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication => sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi}/graphql/GraphQlTester.kt (89%) rename FloconAndroid/{sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication => sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi}/grpc/InitializeGrpc.kt (94%) rename FloconAndroid/{sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication => sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi}/images/InitializeImages.kt (89%) rename FloconAndroid/{sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication => sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi}/sharedpreferences/Datastores.kt (94%) create mode 100644 FloconAndroid/sample-multiplatform/src/androidMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/PlatformSpecificTests.android.kt rename FloconAndroid/{sample-android-only/src/main => sample-multiplatform/src/androidMain}/proto/helloworld.proto (100%) create mode 100644 FloconAndroid/sample-multiplatform/src/desktopMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/PlatformSpecificTests.desktop.kt create mode 100644 FloconAndroid/sample-multiplatform/src/iosMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/PlatformSpecificTests.ios.kt diff --git a/FloconAndroid/database/room/build.gradle.kts b/FloconAndroid/database/room/build.gradle.kts index 86da03840..1df9411b7 100644 --- a/FloconAndroid/database/room/build.gradle.kts +++ b/FloconAndroid/database/room/build.gradle.kts @@ -10,7 +10,6 @@ kotlin { implementation(projects.flocon) api(projects.database.core) implementation(libs.androidx.room.runtime) - implementation(libs.androidx.room.sqlite.wrapper) implementation(libs.androidx.sqlite.bundled) } } @@ -18,6 +17,7 @@ kotlin { val androidMain by getting { dependencies { implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.room.sqlite.wrapper) } } diff --git a/FloconAndroid/database/room/src/iosMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.ios.kt b/FloconAndroid/database/room/src/iosMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.ios.kt new file mode 100644 index 000000000..c9c874bdb --- /dev/null +++ b/FloconAndroid/database/room/src/iosMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.ios.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.flocon.database.room + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.dsl.FloconMarker + +internal actual class FloconRoomDatabaseProviderImpl actual constructor( + private val context: FloconContext, + paths: List +) : FloconRoomDatabaseProvider { + + actual override fun register() { + // no op on iOS + } + + @OptIn(FloconMarker::class) + actual override fun getAllDataBases(registeredDatabases: List): List { + return emptyList() + } +} diff --git a/FloconAndroid/database/room/src/jvmMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.jvm.kt b/FloconAndroid/database/room/src/jvmMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.jvm.kt new file mode 100644 index 000000000..d81f8c8b1 --- /dev/null +++ b/FloconAndroid/database/room/src/jvmMain/kotlin/io/github/openflocon/flocon/database/room/FloconRoomDatabaseProviderImpl.jvm.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.flocon.database.room + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.database.core.model.FloconDatabaseModel +import io.github.openflocon.flocon.dsl.FloconMarker + +internal actual class FloconRoomDatabaseProviderImpl actual constructor( + private val context: FloconContext, + paths: List +) : FloconRoomDatabaseProvider { + + actual override fun register() { + // no op on JVM + } + + @OptIn(FloconMarker::class) + actual override fun getAllDataBases(registeredDatabases: List): List { + return emptyList() + } +} diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt deleted file mode 100644 index bdf02ebb7..000000000 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt +++ /dev/null @@ -1,127 +0,0 @@ -package io.github.openflocon.flocon.plugins.database - -import androidx.sqlite.SQLiteConnection -import androidx.sqlite.driver.NativeSQLiteDriver -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.FloconFileDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel -import platform.Foundation.NSFileManager -import platform.posix.close - -internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { - return FloconDatabaseDataSourceIos(context) -} - -internal class FloconDatabaseDataSourceIos( - private val context: FloconContext -) : FloconDatabaseDataSource { - - override fun executeSQL( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteSqlResponse { - val fileManager = NSFileManager.defaultManager - if (!fileManager.fileExistsAtPath(databaseName)) { - return DatabaseExecuteSqlResponse.Error( - message = "Database file not found: $databaseName", - originalSql = query - ) - } - - val driver = NativeSQLiteDriver() - val connection = driver.open(fileName = databaseName) - - return try { - val firstWord = getFirstWord(query).uppercase() - when (firstWord) { - "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) - "INSERT" -> executeInsert(connection, query) - "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) - else -> executeRawQuery(connection, query) - } - } catch (t: Throwable) { - DatabaseExecuteSqlResponse.Error( - message = t.message ?: "Error executing SQL", - originalSql = query - ) - } finally { - connection.close() - } - } - - override fun getAllDataBases( - registeredDatabases: List - ): List { - val fileManager = NSFileManager.defaultManager - return registeredDatabases.mapNotNull { - if(it is FloconFileDatabaseModel) { - if (fileManager.fileExistsAtPath(it.absolutePath)) { - DeviceDataBaseDataModel( - id = it.absolutePath, - name = it.displayName - ) - } else null - } else null - } - } -} - -// --- SQL execution helpers --- - -private fun executeSelect(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { - val cursor = connection.prepare(query).use { statement -> - val columnCount = statement.getColumnCount() - val columns = (0 until columnCount).map { statement.getColumnName(it) } - val rows = mutableListOf>() - - while (statement.step()) { - val row = (0 until columnCount).map { idx -> - statement.getText(idx) - } - rows.add(row) - } - - statement.close() // maybe remove - DatabaseExecuteSqlResponse.Select(columns, rows) - } - return cursor -} - -private fun executeUpdateDelete(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { - connection.prepare(query).use { statement -> - statement.close() - } - // sqlite-kt n'expose pas encore `changes()`, on renvoie 0 - return DatabaseExecuteSqlResponse.UpdateDelete(affectedCount = 0) -} - -private fun executeInsert(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { - connection.prepare(query).use { statement -> - statement.close() - } - - // Récupération du dernier ID inséré - var id = -1L - connection.prepare("SELECT last_insert_rowid()").use { - id = if (it.step()) it.getLong(0) else -1L - it.close() // maybe remove - } - - return DatabaseExecuteSqlResponse.Insert(id) -} - -private fun executeRawQuery(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { - connection.prepare(query).use { statement -> - statement.close() // maybe remove - } - return DatabaseExecuteSqlResponse.RawSuccess -} - -// --- Utilities --- -private fun getFirstWord(s: String): String { - val trimmed = s.trim() - val firstSpace = trimmed.indexOf(' ') - return if (firstSpace >= 0) trimmed.substring(0, firstSpace) else trimmed -} diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.jvm.kt deleted file mode 100644 index dc49389d3..000000000 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.jvm.kt +++ /dev/null @@ -1,129 +0,0 @@ -package io.github.openflocon.flocon.plugins.database - -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.FloconFileDatabaseModel -import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel -import java.io.File -import java.sql.Connection -import java.sql.DriverManager -import java.sql.ResultSet -import java.sql.Statement -import java.util.Locale - -internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { - return FloconDatabaseDataSourceJvm(context) -} - -internal class FloconDatabaseDataSourceJvm( - private val context: FloconContext -) : FloconDatabaseDataSource { - - override fun executeSQL( - registeredDatabases: List, - databaseName: String, - query: String - ): DatabaseExecuteSqlResponse { - var connection: Connection? = null - return try { - val dbFile = File(databaseName) - if (!dbFile.exists()) { - return DatabaseExecuteSqlResponse.Error( - message = "Database file not found: ${dbFile.absolutePath}", - originalSql = query - ) - } - - connection = DriverManager.getConnection("jdbc:sqlite:${dbFile.absolutePath}") - val firstWord = getFirstWord(query).uppercase(Locale.getDefault()) - - when (firstWord) { - "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) - "INSERT" -> executeInsert(connection, query) - "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) - else -> executeRawQuery(connection, query) - } - } catch (t: Throwable) { - DatabaseExecuteSqlResponse.Error( - message = t.message ?: "Error executing SQL", - originalSql = query, - ) - } finally { - connection?.close() - } - } - - override fun getAllDataBases( - registeredDatabases: List - ): List { - return registeredDatabases.mapNotNull { - if(it is FloconFileDatabaseModel) { - if (File(it.absolutePath).exists()) { - DeviceDataBaseDataModel( - id = it.absolutePath, - name = it.displayName, - ) - } else null - } else null - } - - } -} - -// --- SQL execution helpers --- - -private fun executeSelect(connection: Connection, query: String): DatabaseExecuteSqlResponse { - connection.createStatement().use { statement -> - val resultSet = statement.executeQuery(query) - val columns = getColumnNames(resultSet) - val rows = resultSetToList(resultSet, columns.size) - return DatabaseExecuteSqlResponse.Select(columns, rows) - } -} - -private fun executeUpdateDelete(connection: Connection, query: String): DatabaseExecuteSqlResponse { - connection.createStatement().use { statement -> - val count = statement.executeUpdate(query) - return DatabaseExecuteSqlResponse.UpdateDelete(count) - } -} - -private fun executeInsert(connection: Connection, query: String): DatabaseExecuteSqlResponse { - connection.createStatement().use { statement -> - statement.executeUpdate(query, Statement.RETURN_GENERATED_KEYS) - val keys = statement.generatedKeys - val id = if (keys.next()) keys.getLong(1) else -1L - return DatabaseExecuteSqlResponse.Insert(id) - } -} - -private fun executeRawQuery(connection: Connection, query: String): DatabaseExecuteSqlResponse { - connection.createStatement().use { statement -> - statement.execute(query) - return DatabaseExecuteSqlResponse.RawSuccess - } -} - -// --- Utilities --- - -private fun getFirstWord(s: String): String { - val trimmed = s.trim() - val firstSpace = trimmed.indexOf(' ') - return if (firstSpace >= 0) trimmed.substring(0, firstSpace) else trimmed -} - -private fun getColumnNames(rs: ResultSet): List { - val meta = rs.metaData - return (1..meta.columnCount).map { meta.getColumnName(it) } -} - -private fun resultSetToList(rs: ResultSet, columnCount: Int): List> { - val rows = mutableListOf>() - while (rs.next()) { - val row = (1..columnCount).map { idx -> - rs.getObject(idx)?.toString() - } - rows.add(row) - } - return rows -} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.jvm.kt b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.jvm.kt index fad472402..faa603336 100644 --- a/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.jvm.kt +++ b/FloconAndroid/flocon/src/jvmMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.jvm.kt @@ -20,6 +20,7 @@ internal actual fun buildFloconHttpClient(): FloconHttpClient { return FloconHttpClientJvm() } +@OptIn(io.github.openflocon.flocon.dsl.FloconMarker::class) internal class FloconHttpClientJvm() : FloconHttpClient { // client configurable selon la plateforme (Android, iOS, JVM, etc.) diff --git a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.ios.kt b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.ios.kt index 580dd4815..e375bdfc7 100644 --- a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.ios.kt +++ b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.ios.kt @@ -1,7 +1,9 @@ package io.github.openflocon.flocon.network.core.datasource import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.core.FloconEncoder internal actual inline fun buildFloconNetworkDataSource( - context: FloconContext -): FloconNetworkDataSource = FloconNetworkDataSourceImpl() \ No newline at end of file + context: FloconContext, + encoder: FloconEncoder +): FloconNetworkDataSource = FloconNetworkDataSourceImpl(encoder = encoder) \ No newline at end of file diff --git a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt index c062ee677..cb411ccee 100644 --- a/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt +++ b/FloconAndroid/network/core/src/iosMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt @@ -1,9 +1,12 @@ package io.github.openflocon.flocon.network.core.datasource +import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.network.core.model.BadQualityConfig import io.github.openflocon.flocon.network.core.model.MockNetworkResponse -internal class FloconNetworkDataSourceImpl : FloconNetworkDataSource { +internal class FloconNetworkDataSourceImpl( + private val encoder: FloconEncoder +) : FloconNetworkDataSource { override fun saveMocksToFile(mocks: List) { // TODO diff --git a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.jvm.kt b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.jvm.kt index 580dd4815..e375bdfc7 100644 --- a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.jvm.kt +++ b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSource.jvm.kt @@ -1,7 +1,9 @@ package io.github.openflocon.flocon.network.core.datasource import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.core.FloconEncoder internal actual inline fun buildFloconNetworkDataSource( - context: FloconContext -): FloconNetworkDataSource = FloconNetworkDataSourceImpl() \ No newline at end of file + context: FloconContext, + encoder: FloconEncoder +): FloconNetworkDataSource = FloconNetworkDataSourceImpl(encoder = encoder) \ No newline at end of file diff --git a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt index be864eafa..2e175f38d 100644 --- a/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt +++ b/FloconAndroid/network/core/src/jvmMain/kotlin/io/github/openflocon/flocon/network/core/datasource/FloconNetworkDataSourceImpl.kt @@ -1,10 +1,9 @@ package io.github.openflocon.flocon.network.core.datasource import io.github.openflocon.flocon.FloconLogger -import io.github.openflocon.flocon.network.core.mapper.parseBadQualityConfig -import io.github.openflocon.flocon.network.core.mapper.parseMockResponses -import io.github.openflocon.flocon.network.core.mapper.toJsonString -import io.github.openflocon.flocon.network.core.mapper.writeMockResponsesToJson +import io.github.openflocon.flocon.core.FloconEncoder +import io.github.openflocon.flocon.core.decode +import io.github.openflocon.flocon.core.encode import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_BAD_CONFIG_JSON import io.github.openflocon.flocon.network.core.plugin.FLOCON_NETWORK_MOCKS_JSON import io.github.openflocon.flocon.network.core.model.BadQualityConfig @@ -14,6 +13,7 @@ import java.io.FileInputStream import java.io.FileOutputStream internal class FloconNetworkDataSourceImpl( + private val encoder: FloconEncoder ) : FloconNetworkDataSource { private val baseDir: File = File(System.getProperty("user.home"), ".flocon") @@ -27,7 +27,7 @@ internal class FloconNetworkDataSourceImpl( override fun saveMocksToFile(mocks: List) { try { val file = File(baseDir, FLOCON_NETWORK_MOCKS_JSON) - val jsonString = writeMockResponsesToJson(mocks) + val jsonString = encoder.encode(mocks) FileOutputStream(file).use { it.write(jsonString.toByteArray(Charsets.UTF_8)) } @@ -46,7 +46,7 @@ internal class FloconNetworkDataSourceImpl( val jsonString = FileInputStream(file).use { it.readBytes().toString(Charsets.UTF_8) } - parseMockResponses(jsonString) + encoder.decode>(jsonString).orEmpty() } catch (t: Throwable) { FloconLogger.logError("issue in loadMocksFromFile", t) emptyList() @@ -63,7 +63,7 @@ internal class FloconNetworkDataSourceImpl( val jsonString = FileInputStream(file).use { it.readBytes().toString(Charsets.UTF_8) } - parseBadQualityConfig(jsonString) + encoder.decode(jsonString) } catch (t: Throwable) { FloconLogger.logError("issue in loadBadNetworkConfig", t) null @@ -76,7 +76,7 @@ internal class FloconNetworkDataSourceImpl( if (config == null) { file.delete() } else { - val jsonString = config.toJsonString() + val jsonString = encoder.encode(config) FileOutputStream(file).use { it.write(jsonString.toByteArray(Charsets.UTF_8)) } diff --git a/FloconAndroid/sample-android-only/.gitignore b/FloconAndroid/sample-android-only/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/FloconAndroid/sample-android-only/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts deleted file mode 100644 index 545be5263..000000000 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ /dev/null @@ -1,218 +0,0 @@ -import com.google.protobuf.gradle.id -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) - alias(libs.plugins.ksp) - alias(libs.plugins.apollo) - - alias(libs.plugins.protobuf) -} - -android { - namespace = "io.github.openflocon.flocon.myapplication" - compileSdk = 36 - - defaultConfig { - applicationId = "io.github.openflocon.flocon.myapplication" - minSdk = 23 - targetSdk = 36 - versionCode = 1 - versionName = "1.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - val githubToken = System.getenv("GITHUB_TOKEN_GRPC") ?: "" - - signingConfigs { - named("debug") { - // just a dummy keystore to be able to test the release build - keyAlias = "release" - keyPassword = "release" - storeFile = file("release.jks") - storePassword = "release" - } - register("release") { - keyAlias = "release" - keyPassword = "release" - storeFile = file("release.jks") - storePassword = "release" - } - } - - buildTypes { - debug { - buildConfigField("String", "GITHUB_TOKEN", "\"$githubToken\"") - signingConfig = signingConfigs.getByName("debug") - } - release { - isMinifyEnabled = true - buildConfigField("String", "GITHUB_TOKEN", "\"$githubToken\"") - signingConfig = signingConfigs.getByName("release") - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - buildFeatures { - compose = true - buildConfig = true - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -val useMaven = false -dependencies { - if(useMaven) { - val floconVersion = "1.4.0" - implementation("io.github.openflocon:flocon:$floconVersion") - //implementation("io.github.openflocon:flocon-no-op:$floconVersion") - implementation("io.github.openflocon:flocon-grpc-interceptor-lite:$floconVersion") - implementation("io.github.openflocon:flocon-okhttp-interceptor:$floconVersion") - //implementation("io.github.openflocon:flocon-okhttp-interceptor-no-op:$floconVersion") - implementation("io.github.openflocon:flocon-ktor-interceptor:$floconVersion") - } else { - debugImplementation(projects.flocon) - releaseImplementation(projects.floconNoOp) - - debugImplementation(projects.deeplinks) - releaseImplementation(projects.deeplinksNoOp) - - debugImplementation(projects.tables) - releaseImplementation(projects.tablesNoOp) - - debugImplementation(projects.analytics) - releaseImplementation(projects.analyticsNoOp) - - debugImplementation(projects.crashreporter) - releaseImplementation(projects.crashreporterNoOp) - - debugImplementation(project(":database:room")) - releaseImplementation(project(":database:room-no-op")) - debugImplementation(project(":database:room3")) - releaseImplementation(project(":database:room3-no-op")) - - debugImplementation(projects.network.okhttpInterceptor) - releaseImplementation(projects.network.okhttpInterceptorNoOp) - - implementation(projects.grpc.grpcInterceptorLite) - - debugImplementation(projects.network.ktorInterceptor) - releaseImplementation(projects.network.ktorInterceptorNoOp) - - debugImplementation(projects.datastores) - releaseImplementation(projects.datastoresNoOp) - } - - - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) - testImplementation(libs.junit) - debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) - - implementation(platform(libs.kotlinx.coroutines.bom)) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.android) - - // region okhttp - implementation(platform(libs.okhttp.bom)) - implementation(libs.okhttp) - // endregion - - // region grpc - implementation(libs.grpc.android) - implementation(libs.grpc.kotlin.stub) - implementation(libs.grpc.protobuf.lite) - implementation(libs.grpc.okhttp) - implementation(libs.protobuf.kotlin.lite) - // endregion - - // region coil - implementation(libs.coil.compose) - implementation(libs.coil.network.okhttp) - // endregion - - // region room - implementation(libs.androidx.room.runtime) - ksp(libs.androidx.room.compiler) - // endregion - - // region graphql - implementation(libs.apollo.runtime) - //implementation(libs.apollo.http.okhttprealization) - // endregion - - // region ktor - implementation(libs.ktor.client.cio) - implementation(libs.ktor.client.okhttp) - implementation(libs.ktor.client.core) - //endregion - - // region datastore - implementation(libs.androidx.datastore.preferences) - // endregion -} - -apollo { - service("github") { - packageName.set("com.github") - } -} - -protobuf { - protoc { - artifact = libs.protobuf.protoc.get().toString() - } - - generateProtoTasks { - val protocGenJava = libs.grpc.gen.java.get().toString() - val protocGenKotlin = libs.grpc.gen.kotlin.get().toString() + ":jdk8@jar" - - plugins { - id("java") { - artifact = protocGenJava - } - id("grpc") { - artifact = protocGenJava - } - id("grpckt") { - artifact = protocGenKotlin - } - } - - all().forEach { - it.plugins { - id("java") { - option("lite") - } - id("grpc") { - option("lite") - } - id("grpckt") { - option("lite") - } - } - it.builtins { - id("kotlin") { - option("lite") - } - } - } - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/proguard-rules.pro b/FloconAndroid/sample-android-only/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/FloconAndroid/sample-android-only/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/AndroidManifest.xml b/FloconAndroid/sample-android-only/src/main/AndroidManifest.xml deleted file mode 100644 index 8131109c0..000000000 --- a/FloconAndroid/sample-android-only/src/main/AndroidManifest.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/ic_launcher-playstore.png b/FloconAndroid/sample-android-only/src/main/ic_launcher-playstore.png deleted file mode 100644 index 72d7b88d3192f802962f78b0741360f327dc4504..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 292223 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%0A>RbBF?oV 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

4etMG3CGJSvd;*` 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

20VM$1Htsxl!0Q9R2(lgQPCW)`1bgOzzOh^+ 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 iUVqTl;GwV!fd2+=PguU_<(()10000?, authType: String?, socket: Socket?) {} - override fun checkServerTrusted(chain: Array?, authType: String?, socket: Socket?) {} - override fun checkClientTrusted(chain: Array?, authType: String?, engine: SSLEngine?) {} - override fun checkServerTrusted(chain: Array?, authType: String?, engine: SSLEngine?) {} - override fun getAcceptedIssuers(): Array = emptyArray() - override fun checkClientTrusted(chain: Array?, authType: String?) {} - override fun checkServerTrusted(chain: Array?, authType: String?) {} - } - } - } - } - - val okHttpClient = HttpClient(OkHttp) { - install(FloconKtorPlugin) { - //isImage = { - // it.responseContentType?.startsWith("image/") == true - //} - } - } - - fun call() { - GlobalScope.launch(Dispatchers.IO) { - try { - val response = okHttpClient.post("https://jsonplaceholder.typicode.com/posts") { - setBody("{ \"test\" : \"yes\" }") - } - - if (response.status.value in 200..299) { - val responseBody: String = response.body() - println("SUCCESS: $responseBody") - } else { - println("FAILURE: ${response.status}") - } - } catch (t: Throwable) { - println("ERROR: ${t.message}") - t.printStackTrace() - } - } - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt deleted file mode 100644 index f808131a7..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ /dev/null @@ -1,247 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package io.github.openflocon.flocon.myapplication - -import android.os.Bundle -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import io.github.openflocon.flocon.FloconContext -import io.github.openflocon.flocon.database.core.FloconDatabase -import io.github.openflocon.flocon.database.room.room -import io.github.openflocon.flocon.myapplication.database.DogDatabase -import io.github.openflocon.flocon.myapplication.database.initializeDatabases -import io.github.openflocon.flocon.myapplication.database.initializeInMemoryDatabases -import io.github.openflocon.flocon.myapplication.database.model.DogEntity -import io.github.openflocon.flocon.myapplication.grpc.GrpcController -import io.github.openflocon.flocon.myapplication.table.initializeTable -import io.github.openflocon.flocon.myapplication.ui.ImagesListView -import io.github.openflocon.flocon.myapplication.ui.theme.MyApplicationTheme -import io.github.openflocon.flocon.network.core.FloconNetwork -import io.github.openflocon.flocon.okhttp.FloconOkhttpInterceptor -import io.github.openflocon.flocon.analytics.FloconAnalytics -import io.github.openflocon.flocon.deeplinks.FloconDeeplinks -import io.github.openflocon.flocon.tables.FloconTable -import io.github.openflocon.flocon.startFlocon -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import okhttp3.OkHttpClient -import kotlin.random.Random -import kotlin.uuid.ExperimentalUuidApi - -class MainActivity : ComponentActivity() { - - lateinit var inMemoryDb: DogDatabase - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - - intent.data?.let { - Toast.makeText(this, "opend with : $it", Toast.LENGTH_LONG).show() - } - - initFlocon() - - val okHttpClient = OkHttpClient() - .newBuilder() - .addInterceptor( - FloconOkhttpInterceptor( - isImage = { - it.request.url.toString().contains("picsum") - }, - /*shouldLog = { - val url = it.request().url.toString() - println("url: $url") - url.contains("1").not() - }*/ - ) - ) - .build() - -// initializeSharedPreferences(applicationContext) - initializeDatabases(context = applicationContext) - -// FloconLogger.enabled = true -// Flocon.initialize(this) - inMemoryDb = initializeInMemoryDatabases(applicationContext) - -// initializeSharedPreferencesAfterInit(applicationContext) -// initializeDatastores(applicationContext) - - val dummyHttpCaller = DummyHttpCaller(client = okHttpClient) -// val dummyWebsocketCaller = DummyWebsocketCaller(client = okHttpClient) -// GlobalScope.launch { dummyWebsocketCaller.connectToWebsocket() } -// val graphQlTester = GraphQlTester(client = okHttpClient) -// initializeImages(context = this, okHttpClient = okHttpClient) -// initializeDashboard(this) - initializeTable() - - setContent { - MyApplicationTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - val scope = rememberCoroutineScope() - val context = LocalContext.current - - Column( - Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - Button( - onClick = { - dummyHttpCaller.call() - } - ) { - Text("okhttp test") - } - Button( - onClick = { - //dummyHttpCaller.callGzip() - } - ) { - Text("okhttp gzip test") - } - Button( - onClick = { - GlobalScope.launch { - //graphQlTester.fetchViewerInfo() - } - } - ) { - Text("graphql test") - } - Button( - onClick = { - GlobalScope.launch { - GrpcController.sayHello() - } - } - ) { - Text("grpc test") - } - Button( - onClick = { - DummyHttpKtorCaller.call() - } - ) { - Text("ktor test") - } - Button( - onClick = { - //dummyWebsocketCaller.send(Uuid.random().toString()) - } - ) { - Text("websocket test") - } - Button( - onClick = { - throw Throwable("my custom crash") - } - ) { - Text("crash") - } - Button( - onClick = { - Random.nextInt(from = 0, until = 1000).toString() -// Flocon.table("analytics").log( -// "name" toParam "new name $value", -// "value1" toParam "value1 $value", -// "value2" toParam "value2 $value", -// ) - } - ) { - Text("send table event") - } - Button( - onClick = { -// Flocon.analytics("firebase").logEvents( -// AnalyticsEvent( -// eventName = "clicked user", -// "userId" analyticsProperty "1024", -// "username" analyticsProperty "florent", -// "index" analyticsProperty "3", -// ), -// AnalyticsEvent( -// eventName = "opened profile", -// "userId" analyticsProperty "2048", -// "username" analyticsProperty "kevin", -// "age" analyticsProperty "34", -// ), -// ) - } - ) { - Text("send analytics event") - } - Button( - onClick = { - scope.launch { - DogDatabase.getDatabase(context).dogDao().insertDog( - DogEntity( - id = System.currentTimeMillis(), - name = "Flocon", - breed = "Golden Retriever ${System.currentTimeMillis()}", - age = 6, - pictureUrl = "https://picsum.photos/501/500.jpg", - ) - ) - } - } - ) { - Text("Insert dog in DB") - } - } - - ImagesListView(modifier = Modifier.fillMaxSize()) - } - } - } - } - } - - private fun initFlocon() { - startFlocon(FloconContext(this)) { - install(FloconDeeplinks) { - deeplink("flocon://home") - deeplink("flocon://test") - deeplink("flocon://user/[userId]") { - label = "User" - "userId" withAutoComplete listOf("Florent", "David", "Guillaume") - } - deeplink("flocon://post/[postId]?comment=[commentText]") { - label = "Post" - description = "Open a post and send a comment" - } - } - - install(FloconNetwork) - install(FloconTable) - install(FloconAnalytics) - install(FloconDatabase) { - room() - } - } - } - -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt deleted file mode 100644 index e0761a5d8..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/InitializeDashboard.kt +++ /dev/null @@ -1,194 +0,0 @@ -package io.github.openflocon.flocon.myapplication.dashboard - -import android.util.Log -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.lifecycle.lifecycleScope -import com.apollographql.apollo.api.label -import io.github.openflocon.flocon.myapplication.dashboard.device.deviceFlow -import io.github.openflocon.flocon.myapplication.dashboard.device.initializeDeviceFlow -import io.github.openflocon.flocon.myapplication.dashboard.tokens.tokensFlow -import io.github.openflocon.flocon.myapplication.dashboard.user.userFlow -import io.github.openflocon.flocon.plugins.dashboard.floconDashboard -import io.github.openflocon.flocon.plugins.dashboard.dsl.button -import io.github.openflocon.flocon.plugins.dashboard.dsl.checkBox -import io.github.openflocon.flocon.plugins.dashboard.dsl.html -import io.github.openflocon.flocon.plugins.dashboard.dsl.json -import io.github.openflocon.flocon.plugins.dashboard.dsl.markdown -import io.github.openflocon.flocon.plugins.dashboard.dsl.plainText -import io.github.openflocon.flocon.plugins.dashboard.dsl.text -import io.github.openflocon.flocon.plugins.dashboard.dsl.textField -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -fun initializeDashboard(activity: ComponentActivity) { - initializeDeviceFlow(activity) - activity.lifecycleScope.launch { - floconDashboard(id = "main") { - section(name = "User", userFlow) { user -> - text(label = "username", value = user.userName) - text(label = "fullName", value = user.fullName, color = Color.Red.toArgb()) - text(label = "user id", value = user.id) - label(label = "actions :") - button( - text = "Change User Id", - id = "changeUserId", - onClick = { - userFlow.update { it.copy(userName = "__flo__") } - } - ) - textField( - label = "Update Name", - placeHolder = "name", - id = "changeUserName", - value = user.fullName, - onSubmitted = { value -> - userFlow.update { it.copy(fullName = value) } - }) - } - section(name = "Device", deviceFlow) { device -> - text(label = "name", value = device?.name ?: "") - text(label = "androidVersion", value = device?.androidVersion ?: "") - text(label = "language", value = device?.language ?: "") - text(label = "width", value = device?.width?.toString() ?: "") - text(label = "height", value = device?.height?.toString() ?: "") - text(label = "density", value = device?.density?.toString() ?: "") - checkBox( - id = "darkTheme", - label = "darkTheme", - value = device?.darkTheme ?: false, - onUpdated = { newValue -> - deviceFlow.update { - it?.copy( - darkTheme = newValue, - ) - } - } - ) - } - section(name = "Tokens", tokensFlow) { tokens -> - text(label = "accessToken", value = tokens.accessToken) - text(label = "refreshToken", value = tokens.refreshToken) - text(label = "expiration", value = tokens.expiration) - button( - text = "Clear Access Token", - id = "clearAccessToken", - onClick = { - activity.lifecycleScope.launch(Dispatchers.Main) { - tokensFlow.update { - it.copy(accessToken = "") - } - Toast.makeText( - activity, - "cleaned access token", - Toast.LENGTH_LONG - ).show() - } - } - ) - } - } - } - activity.lifecycleScope.launch { - floconDashboard(id = "plainText") { - section(name = "Test") { - plainText( - label = "lorem", - value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eget ullamcorper elit. Pellentesque turpis ex, cursus cursus urna sed, iaculis sagittis nisl. Curabitur vehicula nunc eu metus rhoncus placerat. Vivamus at placerat ligula. Morbi ullamcorper cursus tellus, vitae molestie lorem sollicitudin euismod. Sed ullamcorper, risus vitae facilisis tempor, elit leo accumsan purus, ut ultricies augue erat et justo. Duis efficitur mauris eu finibus tincidunt. Aenean magna libero, auctor quis turpis et, viverra porta lorem. Ut tempus odio sit amet vestibulum condimentum. Donec et augue quis arcu blandit sodales. In laoreet odio id turpis ultricies, eu ornare dui blandit. Morbi hendrerit velit turpis, eget ornare ex consequat id. Nullam rhoncus, libero et sollicitudin tristique, risus ipsum luctus neque, ultricies ullamcorper felis metus non turpis. Nullam sed accumsan sem, at fermentum tortor." - ) - json( - label = "json", value = """ - { - "testData": { - "name": "John Doe", - "age": 30, - "isStudent": false, - "courses": [ - { - "title": "History I", - "credits": 3 - }, - { - "title": "Math II", - "credits": 4 - } - ], - "address": { - "street": "123 Main St", - "city": "Anytown", - "zipCode": "12345" - } - }, - "status": "success", - "message": "Test data loaded successfully." - } - """.trimIndent() - ) - } - } - } - - activity.lifecycleScope.launch { - floconDashboard(id = "form") { - form( - name = "Test form", - submitText = "Submit form test button", - onSubmitted = { values -> - values.forEach { (key, value) -> - Log.e("TAG", "$key - $value") - // input_1 - test - // checkbox_1 - true - activity.lifecycleScope.launch(Dispatchers.Main) { - Toast.makeText( - activity, - "submitted : $values", - Toast.LENGTH_LONG - ).show() - } - } - } - ) { - textField( - id = "input_1", - label = "Input field 1", - placeHolder = "placeholder", - value = "test" - ) - checkBox( - id = "checkbox_1", - label = "Checkbox 1", - value = true, - ) - } - } - } - activity.lifecycleScope.floconDashboard(id = "markdown") { - section(name = "Markdown") { - markdown( - label = "Release Note", - value = """ - # Release Note - - This is a **markdown** text. - - * [x] item 1 - * [ ] item 2 - - Hello `world` ! - """.trimIndent() - ) - } - } - activity.lifecycleScope.floconDashboard(id = "html") { - section(name = "HTML Section") { - html( - label = "HtmlTitle", - value = "

Title

Paragraph with bold text.

" - ) - } - } - -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/device/Device.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/device/Device.kt deleted file mode 100644 index 4c5b75293..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/device/Device.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.github.openflocon.flocon.myapplication.dashboard.device - -import android.app.Activity -import android.os.Build -import android.util.DisplayMetrics -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update - - -data class Device( - val name: String, - val androidVersion: String, - val language: String, - val width: Int, - val height: Int, - val density: Float, - val darkTheme: Boolean, -) - -val deviceFlow = MutableStateFlow(null) - -fun initializeDeviceFlow(activity: Activity) { - val language = activity.resources.configuration.locales.get(0).language - - val displayMetrics = DisplayMetrics() - activity.windowManager.defaultDisplay.getMetrics(displayMetrics) - - deviceFlow.update { - Device( - name = "${Build.BRAND} ${Build.MODEL}", - androidVersion = Build.VERSION.RELEASE, - language = language, - width = displayMetrics.widthPixels, - height = displayMetrics.heightPixels, - density = displayMetrics.density, - darkTheme = true, - ) - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/tokens/Tokens.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/tokens/Tokens.kt deleted file mode 100644 index 19e9972a2..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/tokens/Tokens.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.openflocon.flocon.myapplication.dashboard.tokens - -import kotlinx.coroutines.flow.MutableStateFlow - -data class Tokens( - val accessToken: String, - val refreshToken: String, - val expiration: String, -) - -val tokensFlow = MutableStateFlow( - Tokens( - accessToken = "1234567890", - refreshToken = "dndkjcncjzaksp", - expiration = "12:30:43" - ) -) diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/user/User.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/user/User.kt deleted file mode 100644 index 0de793ad8..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/dashboard/user/User.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.openflocon.flocon.myapplication.dashboard.user - -import kotlinx.coroutines.flow.MutableStateFlow - -data class User( - val id: String, - val userName: String, - val fullName: String, -) - -val userFlow = MutableStateFlow( - User( - id = "1234", - userName = "flo", - fullName = "Florent", - ) -) diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt deleted file mode 100644 index e3c13da9c..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/DogDatabase.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import io.github.openflocon.flocon.database.room.extensions.floconLogs -import io.github.openflocon.flocon.myapplication.database.dao.DogDao -import io.github.openflocon.flocon.myapplication.database.model.DogEntity -import io.github.openflocon.flocon.myapplication.database.model.HumanEntity -import io.github.openflocon.flocon.myapplication.database.model.HumanWithDogEntity - -@Database( - entities = [ - DogEntity::class, - HumanEntity::class, - HumanWithDogEntity::class, - ], - version = 3, - exportSchema = false, -) -abstract class DogDatabase : RoomDatabase() { - abstract fun dogDao(): DogDao - - companion object { - @Volatile - private var INSTANCE: DogDatabase? = null - - fun getDatabase(context: Context): DogDatabase { - val dbName = "dogs_database" - return INSTANCE ?: synchronized(this) { - val instance = Room.databaseBuilder( - context.applicationContext, - DogDatabase::class.java, - dbName - ) - .floconLogs() - .fallbackToDestructiveMigration(dropAllTables = true) - .build() - INSTANCE = instance - instance - } - } - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/FoodDatabase.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/FoodDatabase.kt deleted file mode 100644 index 035c1b8de..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/FoodDatabase.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import io.github.openflocon.flocon.myapplication.database.dao.FoodDao -import io.github.openflocon.flocon.myapplication.database.model.FoodEntity - -@Database( - entities = [FoodEntity::class], - version = 1, - exportSchema = false, -) -abstract class FoodDatabase : RoomDatabase() { - abstract fun foodDao(): FoodDao - - companion object { - @Volatile - private var INSTANCE: FoodDatabase? = null - - fun getDatabase(context: Context): FoodDatabase { - return INSTANCE ?: synchronized(this) { - val instance = Room.databaseBuilder( - context.applicationContext, - FoodDatabase::class.java, - "food_database" // Nom du fichier de la base de données FoodEntity - ).build() - INSTANCE = instance - instance - } - } - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt deleted file mode 100644 index d380b0602..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/InitializeDatabases.kt +++ /dev/null @@ -1,146 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database - -import android.content.Context -import androidx.room.Room -import io.github.openflocon.flocon.database.room.floconRegisterDatabase -import io.github.openflocon.flocon.myapplication.database.model.DogEntity -import io.github.openflocon.flocon.myapplication.database.model.FoodEntity -import io.github.openflocon.flocon.myapplication.database.model.HumanEntity -import io.github.openflocon.flocon.myapplication.database.model.HumanWithDogEntity -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch - -fun initializeInMemoryDatabases(context: Context): DogDatabase { - return Room.inMemoryDatabaseBuilder( - context, - DogDatabase::class.java, - ).build().also { - floconRegisterDatabase( - displayName = "inmemory_dogs", - database = it - ) - } -} - -fun initializeDatabases(context: Context) { - val dogDatabase = DogDatabase.getDatabase(context) - val foodDatabase = FoodDatabase.getDatabase(context) - - floconRegisterDatabase( - displayName = "dogs", - database = dogDatabase - ) - - GlobalScope.launch { - dogDatabase.dogDao().insertDog( - DogEntity( - id = 1, name = "Flocon", breed = "Golden Retriever", age = 6, - pictureUrl = "https://picsum.photos/501/500.jpg", - ) - ) - dogDatabase.dogDao().insertHuman( - HumanEntity( - id = 1, - firstName = "Florent", - name = "Champigny", - ) - ) - dogDatabase.dogDao().insertHuman( - HumanEntity( - id = 2, - firstName = "Camille", - name = "Champigny", - ) - ) - dogDatabase.dogDao().insertHumanWithDogEntity( - HumanWithDogEntity( - humanId = 1, // florent - dogId = 1, - ) - ) - dogDatabase.dogDao().insertHumanWithDogEntity( - HumanWithDogEntity( - humanId = 2, // camille - dogId = 1, - ) - ) - - dogDatabase.dogDao().insertDog( - DogEntity( - id = 2, name = "Vanille", breed = "Basset", age = 2, - pictureUrl = "https://picsum.photos/501/501.jpg", - ) - ) - dogDatabase.dogDao().insertHuman( - HumanEntity( - id = 3, - firstName = "Auguste", - name = "Dum", - ) - ) - dogDatabase.dogDao().insertHumanWithDogEntity( - HumanWithDogEntity( - humanId = 3, // auguste - dogId = 2, - ) - ) - - dogDatabase.dogDao().insertDog( - DogEntity( - id = 3, name = "Scoubi", breed = "Yorkshire", age = 2, - pictureUrl = "https://picsum.photos/501/502.jpg", - ) - ) - dogDatabase.dogDao().insertHuman( - HumanEntity( - id = 4, - firstName = "Jean", - name = "Paul", - ) - ) - dogDatabase.dogDao().insertHumanWithDogEntity( - HumanWithDogEntity( - humanId = 4, // auguste - dogId = 3, - ) - ) - - val longBreeds = listOf( - "Golden Retriever royal de la lignée légendaire des chiens des montagnes dorées du nord-ouest canadien, " + - "descendant direct des plus nobles compagnons de chasse de l’ère victorienne, connu pour sa loyauté infinie, " + - "sa douceur de caractère et sa passion inébranlable pour les flaques d’eau et les bâtons jetés dans les rivières.", - - "Basset des plaines méridionales au flair infaillible, issu d’une dynastie ancienne de chiens détectives spécialisés " + - "dans la recherche de friandises disparues, capable de renifler la moindre trace de biscuit à plusieurs kilomètres, " + - "tout en gardant un regard plein de mélancolie et de sagesse millénaire.", - - "Husky sibérien au pelage argenté, né pour courir dans les neiges éternelles et hurler à la lune les chants oubliés " + - "des aurores boréales. Sa résistance légendaire et son regard bleu perçant en font un symbole de liberté, " + - "d’endurance et d’amitié indestructible entre l’homme et le chien.", - - "Berger australien des terres rouges, gardien des troupeaux et des rêves, au regard vif et au cœur débordant d’énergie. " + - "Il danse entre les collines et les vents, tissant des liens invisibles entre la nature et l’esprit des vivants, " + - "incarnation du courage et de la curiosité sans limites." - ) - - for (i in 1..20) { - val randomBreed = longBreeds.random() - - dogDatabase.dogDao().insertDog( - DogEntity( - id = 10L + i, - name = "dog$i", - breed = randomBreed, - pictureUrl = "https://picsum.photos/500/50${i % 10}.jpg", - age = (1..15).random() - ) - ) - } - - foodDatabase.foodDao().insertFood( - FoodEntity( - id = 1, name = "Cheese Burger", type = "Sandwitch", calories = 1234, - ) - ) - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/dao/DogDao.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/dao/DogDao.kt deleted file mode 100644 index 4905192ab..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/dao/DogDao.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Upsert -import io.github.openflocon.flocon.myapplication.database.model.DogEntity -import io.github.openflocon.flocon.myapplication.database.model.HumanEntity -import io.github.openflocon.flocon.myapplication.database.model.HumanWithDogEntity -import kotlinx.coroutines.flow.Flow - -@Dao -interface DogDao { - @Query("SELECT * FROM DogEntity") - fun getAllDogs(): Flow> - - @Upsert - suspend fun insertDog(dog: DogEntity) - - @Upsert - suspend fun insertHuman(human: HumanEntity) - - @Upsert - suspend fun insertHumanWithDogEntity(humanWithDog: HumanWithDogEntity) -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/dao/FoodDao.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/dao/FoodDao.kt deleted file mode 100644 index 71fd33707..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/dao/FoodDao.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import io.github.openflocon.flocon.myapplication.database.model.FoodEntity -import kotlinx.coroutines.flow.Flow - -// DAO pour la nourriture -@Dao -interface FoodDao { - @Query("SELECT * FROM FoodEntity") - fun getAllFoods(): Flow> - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertFood(food: FoodEntity) -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/DogEntity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/DogEntity.kt deleted file mode 100644 index a6902dbe8..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/DogEntity.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class DogEntity( - @PrimaryKey(autoGenerate = true) val id: Long = 0, - val name: String, - val breed: String, - val pictureUrl: String, - val age: Int, -) - - diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/FoodEntity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/FoodEntity.kt deleted file mode 100644 index abd8c3a47..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/FoodEntity.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -// Pour la base de données "FoodEntity" -@Entity -data class FoodEntity( - @PrimaryKey(autoGenerate = true) val id: Long = 0, - val name: String, - val type: String, // e.g., "fruit", "vegetable", "meat" - val calories: Int, -) \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/HumanEntity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/HumanEntity.kt deleted file mode 100644 index e1f2afcd1..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/HumanEntity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class HumanEntity( - @PrimaryKey(autoGenerate = true) val id: Long = 0, - val firstName: String, - val name: String, -) \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/HumanWithDogEntity.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/HumanWithDogEntity.kt deleted file mode 100644 index 6d117e798..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/database/model/HumanWithDogEntity.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.openflocon.flocon.myapplication.database.model - -import androidx.room.Entity -import androidx.room.ForeignKey - -@Entity( - primaryKeys = ["humanId", "dogId"], - foreignKeys = [ - ForeignKey( - entity = HumanEntity::class, - parentColumns = ["id"], - childColumns = ["humanId"], - onDelete = ForeignKey.CASCADE, - ), - ForeignKey( - entity = DogEntity::class, - parentColumns = ["id"], - childColumns = ["dogId"], - onDelete = ForeignKey.CASCADE, - ) - ] -) -data class HumanWithDogEntity( - val humanId: Long, - val dogId: Long, -) \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt deleted file mode 100644 index 099d6d9f6..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/sharedpreferences/SharedPreferences.kt +++ /dev/null @@ -1,70 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package io.github.openflocon.flocon.myapplication.sharedpreferences - -import android.content.Context -import android.preference.PreferenceManager -import androidx.core.content.edit -import org.json.JSONArray -import org.json.JSONObject -import kotlin.uuid.Uuid -import kotlin.random.Random -import kotlin.uuid.ExperimentalUuidApi - -fun initializeSharedPreferencesAfterInit(context: Context) { - val referencedPref = context.getSharedPreferences("ref_pref", Context.MODE_PRIVATE) - //floconRegisterPreference(FloconSharedPreference("my_custom_name", referencedPref)) - - referencedPref.edit { - putString("works", "yes") - } -} - -fun initializeSharedPreferences(context: Context) { - PreferenceManager.getDefaultSharedPreferences(context).edit { putFloat("flocon_validity", 42f) } - - context.getSharedPreferences("user_pref", Context.MODE_PRIVATE).apply { - edit { - putInt("age", 34) - putBoolean("isHuman", true) - putString("name", "flo") - } - } - - context.getSharedPreferences("settings_pref", Context.MODE_PRIVATE).apply { - edit { - putBoolean("isValid", true) - putString("settings_dummy_variable", "variable value") - putString("settings_dummy_variable_2", "variable value 2") - } - } - - context.getSharedPreferences("groups_pref", Context.MODE_PRIVATE).apply { - edit { - putString("group_one", generateUsersJson(3).toString(4)) - putString("group_two", generateUsersJson(5).toString(4)) - putString("group_three", generateUsersJson(10).toString(4)) - putString("group_four", generateUsersJson(2).toString(4)) - } - } -} - -private fun generateUsersJson(number: Int) : JSONArray { - val usersArray = JSONArray() - - for (i in 1..number) { - val randomUsername = "user_${Uuid.random().toString().substring(0, 8)}" - val randomEmail = "$randomUsername@example.com" - val isActive = Random.nextBoolean() - - val userObject = JSONObject() - userObject.put("id", i) - userObject.put("username", randomUsername) - userObject.put("email", randomEmail) - userObject.put("is_active", isActive) - - usersArray.put(userObject) - } - - return usersArray -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt deleted file mode 100644 index a054832b2..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/table/InitializeTable.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.openflocon.flocon.myapplication.table - -import io.github.openflocon.flocon.Flocon -import io.github.openflocon.flocon.tables.dsl.table -import io.github.openflocon.flocon.tables.tablePlugin - -fun initializeTable() { - Flocon.tablePlugin.table("analytics") { - column("name", "nameValue") - column("value1", "value1Value") - column("value2", "value2Value") - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/ImagesListView.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/ImagesListView.kt deleted file mode 100644 index 84ac6eb8f..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/ImagesListView.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.openflocon.flocon.myapplication.ui - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage - -@Composable -fun ImagesListView(modifier: Modifier = Modifier) { - val images by remember { - mutableStateOf( - List(100) { i -> - "https://picsum.photos/id/$i/1000/1200" - } - ) - } - LazyColumn(modifier = modifier) { - items(images) { - AsyncImage( - model = it, - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .height(200.dp) - ) - } - } -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Color.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Color.kt deleted file mode 100644 index 40469f323..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.openflocon.flocon.myapplication.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Theme.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Theme.kt deleted file mode 100644 index 4be454eda..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Theme.kt +++ /dev/null @@ -1,57 +0,0 @@ -package io.github.openflocon.flocon.myapplication.ui.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun MyApplicationTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Type.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Type.kt deleted file mode 100644 index e79a5567f..000000000 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.openflocon.flocon.myapplication.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/res/drawable/ic_launcher_background.xml b/FloconAndroid/sample-android-only/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index ca3826a46..000000000 --- a/FloconAndroid/sample-android-only/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FloconAndroid/sample-android-only/src/main/res/drawable/ic_launcher_foreground.xml b/FloconAndroid/sample-android-only/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d114..000000000 --- a/FloconAndroid/sample-android-only/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/FloconAndroid/sample-android-only/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 82cd4a2bf..000000000 --- a/FloconAndroid/sample-android-only/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/FloconAndroid/sample-android-only/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 82cd4a2bf..000000000 --- a/FloconAndroid/sample-android-only/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-hdpi/ic_launcher.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 913a32ff0b97c31180c9354a8f1eb66bdeff3956..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5418 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 diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index 09a456c5d3f6b481886ab8f738ea34503d197deb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7736 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 diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 3661e71e90554185bdc67555985fe6725eaa4439..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3256 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 diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp deleted file mode 100644 index d2859f8dc52a51f73739436920694d979336c8d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7706 zcmV+#9_8UuNk&Ez9smGWMM6+kP&iBl9smF@Yrq;135JcNxX~RruD<_*0f!DE`ac2s zIi@U3En9O8x(LoZXu^;De=b}{vTa)Pac%1{jF*2fw$1Ad z+(weDRxOsr3gG1*m28I3V7QHKTUFM)==;Cn$K73`KlEP-1yBLg{|SJWF~=MjaR8Va z1LZGAm=kQF0Rlh(&L-yo*K!}&GbAA;*l(LXI21fn*K(a4ka=WV>Q8&Pk`ow>lB00$ zmC#KCne_~#OW=^h+;|s8QvlZ2awr%^{|aYE~ywm z>$9FqZf)DDwyeF+{lDMQh{zp^paGZE>Z1S}ApK)vcXxQ)v!x0HK;Y3g+qT!%)NHlC zE$NWlY}@uL*l2tJ+rFDU0d}+hUnI$?e^oWZ%=Grm!q71@Gcz+YGc%vX%*@QpOv}s+ z1Gn1@)m_~M|G!}FV)%)r7#ISOeL0-l^2+W2M3u)yA(l1*(VF9q-R8NIhcX#=UCBI| ztjc1Wmu=hT$hvLY_sqVhmu=g2`$pYu zWVx~;893uSJa7AFI@bSxkH_;_wN{cw+T6Bo`(=ZH|ojYi`@NU$3pa?%sK| z744*xwW{hF6W}0-^R9gsmqQ=a>qzYt8fM#0r_u$*TVu&h>N@l|Btj~2MuH>&QjjVj zv4A6-;4J18;7f?UmzX&O-xBlkaEYgTfBMD;PIAY=dFJQB8O$kHJYY`=6OTaH!D}viI1J{&YWCMB4sei< z?n9daIo`@d0Y+ej!5HWxD>n!jKww}nAMvN4*!PP)4wh%NJ^ohot*{amB{23=+BgXC zah=K*eTB~I1?vzZ-UtB3;H7_k&p|_0X)J{fWEc#B0lskz-?;nN*KS)vdDMahSr(Sa zs5y|~ql3Jxw&`Dv5sUyeY%aH+_buz=o?m#&@n7D3>c9Pe)?B6grmGG9;+cD~=l8Uu zNv_;oM$#9-vsw3Tt`7hz<7h=w4#KnAT0g_;L{dXdhUG)H%-{Op!}mRNkC0@`UmkY) zmfQNe4-MVXk}@DT#nK;F|8j4&FSe@yfC!-q>fs3+MU@IF{4=W*8P_at*b34y03e1j zAF8ciz4YL7_w-!5QoZy=*!F8uxVr4V0h?a^p}9|9nf=7+$q&N0|7!HzG&=T)i@B>y z+Tr!8CR9QF6S0VoKz3FWR8$Ob5+dnx07yf5I;Mb3*Itg_spmU!Mb2Gj%~rfz*1ERH zUm0ZdG?*E%#o0z&M$HMXb>$P^J?5J&O^CV9Di*n-GpB;4rQ!AKp&sh%>+1;^1Y;;_ zXbl7WIxCDL2!Nz|H_u&rUDJz{K)1d{eLd;yRFEj z)F>pDVpiCh^gb=nY*&WyworOd4Mkueib#1kXsvdDwVyA^#9F*J=->f>3t zY%#BEv?B`k!sVtIbaYrj&~g|DWl}JL7)4XVRU#{!?KzAx@u_`^}<-hW}*APivEOF{fgZO7HnT$D87ijPN@3#NO4^( z914YDG=M4gfHaw!S6EB~fckhcxdnmB8Wz+nyYvrR_CII-l6B|$dFGd;-a)wh5BJDdLe;-ug(FhOTsHlx zlE73o#D*YS$s|0kBH)n69p$-LQ{Q^EBtld=A-V0AUw*hvY5kDlH(p)2B{1oP2M4dv z7{p~c>p7$*YXfQ`Wn~d(3$}68X7zMD$8GLI?U<0;=wOObVh4Xw`DI@ ze8JCrx>-iR_IT{+F{{O_%l+^}2;#xd2?_?P4b4%<;LQ@D(IhZ=CdW(X_Wx^IgGB^s zMY{-!Fq8rtf#~CajTdde2rFI|tQithB;(pr(RyNR$4B2~^-W^Ot-lf!TqaLheF$cg zRu}RAH3WYy!e(#u|dfYjcD7)MOPCH0#X{=XHTmABZ^c& zS09U_Bm@YW0@}FL|?iUuRqY5qrPbGIbyIE)8?7BG7MQiv|Y_vZdw6B2wE%d za!jLEEXskNDwy@Y_=WO%5my8=*h7itHnc;*iBi-9my-w^!U0P*OQ<2*Ay_-2QmV%2 zn#UWyz=HLXD1cIcmT6|GVk7v@yMRv+TK@IwUGbWIxbKv+me6}xS4r2Zlh=x&QPpF; z(FgE3N_*s++WQ?897g-n&@bO1h;BCf;kWSm2;xlOh|QNmQuwE;L)OlQBjIwx1Ozj> zC)}X5ocwrao9Dm5+PDX~>JMlrBfRZ|k^&ncMCWK#dWy_8TGP~s(I>W4!UeC}FR$Dg zcb|ORa}PdXNKSqvZAhv(EL)X}!BH*fMGdJA?;+H3r{DOog2A)clnZ=s&50unIM^u0 zzsfL3j+M?BFk;KpBY%7uqm?Uv`8hFDKBva;*ZvlVe2UzIn4!TI5j4rPb^cxoUNE7q zz4drY2&W*%vZe7GnYF1Qi3A{-OwcA#nw}*N1SKy`K{^luWT0fHK51W)2*ES)BJJKX z$GEL>%+rTQhj%_1H#q4UsG04BGC1@Hb3KU_7ytg*(wlEs=ZurPAODs%?{m?!jc2H{ zjSz_puTum=IY4A9j0WR(kGb<#^PSRaV0}Q{e_tVv|Mw@oq#&~ms1>Pdi5g?9DH6wL z7*r*61z8`f1i*x6mdL=`Pl5048llT3EvVG$**RY79zh)TdFAdFG01@U)%h-2^mc_bOuX#x;U zwQ3Ab7OfPSjmon@;zrLg=C9a+rVy7n-LUXf7>5Geu^(_mB_ERsNP7sv=uEG+kUx77K^d(xZjx4yB{DiqK|SaOc{gO;^?!J_Z+!Au8tRzPs~&pEDp!(t z#(1PXatSf`-bxVI;&2^fB!@S#3rIEgaymUWT{Dcnus9j}%36!GP|OlW;<3wN!;ICa z30DRvZZW=_KIw}=8Lh=g#bu4nUm_Y z^`{%sD+mgSYJfNmCsU#_Bw+rU{SAKdvm_vDwsnKFkR4yt1SUGc(D$mCAt1EBrC;5PTH(Hzz4pw9%5MLQ6g4XU*Ry1Z z3R=NJb6u5o0^zRxQdfpx6}TL61!Ue0;g$>ENYNPUzP34VY-_pC`>YXbR8y-nOJ#*X z$(OHhIOMyc)%hA)6Ng?3#!w`$CJEsgX4%s7{u$)2hECi!ra&cv7}rBst=i_Fcj(M} zZf}oep)yO5Pv@46)bCjRxh|V(=RJlio=>9oBht(PEeuKJ@!vo$Bapm9NT^9VTR6bGD#PJ^bV~4!HI8dY!7F8NKOv1ERgE zga`zq0IbHXz9;FMavI}kQcW1?8X?0zaeuphQ>T)xlY3U{a}@F@C@OT+%`Tr??6^8{ z618!`qa|YQU@1lc01N6UzBx&@+C7Uw1-Kle4EdYnGsbN>v-z5VaiCu@eW z$E1~#5`Q}2g|SG{?4`p@9D+*0WJQt#DM-1bE_gtl61XQ;bw&Xo3AB!x`QZJ$&ZvsLE*yn!~X%QwFDU1|FiU1;^P_)l+D`rudj=jqo z;pkudodQ%d@0YdbF5W^AEEeIQLZ`LS;CG!$BgbeskGUaw2bS(5{>m?4OeL@oXQa9` zpLFfLvXgN%Ap&7BUW6wlk}nmaANu;CRi1f^`+Ak%6N6G9W)@Sxh>=sfc5@lHr+3WX zpLNBfNmT+`crcpq7DWNkbN~;Ck7ALj(d=?PAN$+>*m-51)AVbO} zzqhYuXUh$;B%dBAIXoWaD8Dvx_kG14y&9?SJBKYNn|nvw#Z6BuT}{o2W;6bKoMavp zit?RC58x+>fEc@~I27TE1TEE5jH4VDi*X`kHqNI;{P(Rpio21zYzUp#v+H_?0UrX7 zBoSev0&2YBV${^|N(ATTNuD47y?+07Y3XZf(;P{S&iU{Ik=u)ugqGwiY(X`G5E7|0 zVVIL!KV5VM#bDc8`L@O`Of`rlmutU<9Q!G0=eUl~Y0NUMihkF0Dgj~Oe<7g{AWXfe z9sLwX%IQ?&%K0f!vCQ(kCrKP*xO>k=iLHN&eEXu?qGX*wyi0*n9ch)O3)RIrDl;YZ3Yn)gWem;LTVsJU zY~It-@>dnE3E%$pJk=CL#6%(`stg;v2Z3bNu(EXG?ymPhSF)l19}&e<0I}7p*_pjj zx1E!OF;-tyM^ooKthMF=7)g=vo8+)|)cDx_+`$O#W6;)|s?L7voB&M#5%p66I(0Hf z?$)I(0M1WsfjAzj*vcfckq2T7YhG_`AuM<%n=j(MPpD+QD$)QT=+%pAmsPxg5Q>AY z9CHthMOzGs^Gu@eXKBF3AVa1Mb+twIa9ln#Rtv`!R=p&pu`*crgo6c_uz5DsFR~a> z%~_y`GOWei!9rImIR%({H8uN#QzROz0FY>-C7a#b^Uc8pg>+D?qfiw{1u!x<043@| zQy3phTpR)o>H4ly0RXxUJ|;`GTtO5mH(zhqa$owUz(u4?r2S}LtXObur<|v3vsG=A zliLq(8SApMHtUMvf+y@{=00)k~B_&cd6xR<-2!iXR{ff+Xl(mi5d% zgs34@HPP*(Rlk+>4H+>?qmC&K6b-yeW@*R-NC7}HKG%`7EMV*RJb?nh)2%(N1r&+0 zs>Dg#Sq~47jQ00>>!iU2!LXR8zh_w{XwpR6kb58WAj$o@S>LLdZ*yF)IY$|V1p zpRA+W)br(X&=K2R4m{}bT$tk6ia5~53bq*R`$}i`O9_E;AfWBe?>fI)^ z`j{ll#*-5pEjYF8&L)aHK`6!;hSe%lxl52UWW6_gwfZtG{h*>SFAJIyZqh9v5s)&N zW<&c*z0wmFLk`)c9b^w~%0tx0AOJ83?mD0IF*Z}sfb4OY;RDVi$b#R{P<8~qT@soW zFZWyHhXkl)W@!J_*Qp#trPmwCUM|-i*ixFqU?%g`(g}0PEEgk1gw=A0r5dL{03#;p zo%atc8g+vx3YsSSJDO`w>@xmNNI%+N{2;7&RK%(lGH?SpRC4PaDmk>ZAgeQyBt`axFxUJA_=HCYubAHu+h4?2ml0cR#?#%!ql-O) zZAhJ{k$DeEkW=VfNu*LLOujljeLkN#_);J;MopUTt6xCV*z_OoHTDauB8dsAlyHEs zpO;#?%uYIe2u`WcQ;(lD4Nd6VIbRF9mWx!2$i#qL)$o#9V`h~WPKQg%Ke-%R`kCDG zTVbhN{%tYRP3L@f_b{e!LdqJOWM0ONHnX+|2!N&tiHZ2cf@Ow? zlFboO=$Pm!WBvvvk~Mx+pXodo(_3n-U=_cZpz?@w1&S4sXj-S-w*zzT;w+_meuZ)( zHMOJtC(goL#4KL%j=OBGh2$QM-p{b@znrNmTmF3^_drdq_0zdw2794)k044RjsrJa zL&2f!5eb6OZ(x}T^oT@48Apv$>gueOH1!&=kNsVH@uyIobS#;RxcaYa)1B@ckO&bN z91Tk1EU0oHAxT^+&dioGAY=iq2OEPnT+ch?j8?p4Q_1Ev_XAI6NSwh0b1U};D25eK zy4|l`A4)A&`>p=Dn6+4d2IjrGy7fO_qFL(Bl(ac&yUOGPz*Hw>mLW6=5<@B!PEu64 zC(kT(tV<$E-KbGVOH{nE|95%IZu#+3T&YjH?UcCFthT*zqQW@me|hnei=W@yaLdW^ zq}?Z7u6qDckCg0lXUdl@VfPog-dKC+!o_8}Od+cH=viDK)R|dVvXBE#=HH=kNMdd+dzFIhvXTOrweEp-X_xB_W795^bUiFk+I}Vvm483Is+8 zg@$7lrZYQ`;m>8dB$a_GpdcDOA+9S(f^vy}xxBU6={0Eh;J zMyzAfrACuTgd#;Yh5GN~4LO|WYi7=V380n`U`l|NAgIODC~XjhL`6a=rzil=Rl$Vn zm_iGIe5?sgeXfNlc@Y3BXnNL*%#J2M>>)kSBUV?Nc8X zsv-bJXbYW{DhzBY3q4SbC)nOEIZM{>y}NhoaFIXYdx}5w1g2%a1<+1G+t36|92SF} zfKs9gC8ti{yC&d?MN3PVDrSVjD#Q#plK0P#?tO7CGY1n(xtyXIHBAEnIw_z{B#8fC z#}FjxncGCD%`Uwrp`*O4ki()2B`z`SZj5h3K0SQ952 zu_B-Vf{H@RR2vk%*x5uG+<2O!W&i)~qu(osz`^(3f9C2)6hT6pU~^F?TR)*RNfeiw zs{^Ef37z;-VmUo4Q*8*uT)o%x#Wp)1aS00ANHxYcv)_GWCByiXwzWh1Gr8*`=Chd* z096qbn1Kqxh@Rlbi2)Lg3L03$gow^p%M-L(T)65@bm_ORKlSpzi$mdH=93Sf1`ZYV zq02Gd6ctL_L*8qd%9d^o_+{tAYU)W$O!23BeTq(#usgf<|`T(u9B zhRT3YcPyF(jgRWNWJ|Io`R0M#)*wTCwdZgCES0%@rFN_J+v_@^>|1$0wY5b`(@>8p z#TjubZ5x^@!BSAr{Z-U=LV^L8;E&2fyJ5xCeDWeg?O^s-V=krKs?)wDJ6keuXFVAy zfHehC5K&Y{x<)k7NxsJTH2Fklw3E>qsTx|1G^mMiNcCgM9Q#_wFDn`TyS%;<_Bbvy zT&A@<{9 diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index f6f9e3b3bcb4f5e3821445a27f65e66a7c67f849..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4358 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 OaH6-heR3PzQ7Hfy@9t^< diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp deleted file mode 100644 index b069263aa50e3dd4d5dc408fea1c602945d8e0be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21662 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 diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 9cfa9820fb71df697a1a9e68662bc54a2ee06b9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15670 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 diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 51464b860da1447ba9cc3f52612c4aed55eeeebf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42028 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 diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index 87523aa7922fb6c7d6dceca86ba100989857e067..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23736 zcmV()K;OSoNk&GnTmS%9MM6+kP&iDaTmS$szrZgL35RXlND|~^#drAM?_^3WUB0DL&SA4rC_a0 z)5sB0mC7nfnca4jB%(-4OF6pIi9kq8Cm=VHe5tPnx>+>kX4{G@lYlFf@(YW|d_Y|> zDSI~nW+p_~VAR;g^aq`6z)v`o$K3Dj=F&%Z~1 z#9;Q)yiyN6VrGVOuWA03+wX(E{s&KRSG^9*m zR47gl|Mu>jXi^bXCLjC^E^8EV`ZMHTuJTtAx3lX%)pm-Oj4KA>Nk}O6SDqlPJzZ3Q2r&`nsXbw%l)R#Ma8Dy=+<9 z^&F0yyw*q`i}UwoD0Nc6IamB@eJ<^midM9Z@lu;D6|K!$EH-9dJbuB=$Ko8H=^%i9 zzqAp(9aCq*~piTd`q6c`RWS4NPpbwMKAi0FB|!?R?M#yhFp5@?dwse z=AV_#ko*F$0O)gvzBOK1TCEJTPRH*H0+r4c`rtPJJO`tqOytXqn3xm=*chfRU*yj) z0|Cmf0K;ks?*jmZNUw8W#+x z9r8fTSXFt_4Fk^PN39t~L6T%Ukv;SFeSZ(h_Z`WJecvUz@B6;oWhy{KX7$8<^Mv&2 z_oFSjW@tI^;Bet!d9$IU7Z(hA(7pGS3*|>!ASjwNXwkQzpx1`yN!u{A(OYPL&xAu| z!#ch^s$B{iaAZTcB&dW#CED7L52B5qBeFpS6E_tMUoIXSWWj?&13wqW26=IzJecom z7m6xvkWkyUsx5QhPkuash4}vj)Swvws6Ymn0`Bhmy?3|l$hK8m8Dk!Kg1ZFZ4k?i? zaX5TEf9LGUxNRFrbmrWBEw=w&GXZ+H|66X!Ql961mu{w5uE)lGj&m4y!`#UC8r z6z9QVB*fj_eNEU0pB=6VUmR9=CfvBB4u$;-E^ES#yA{sD*|=m=SU3>5)m%g_6VC3i zu8YW}iQvVZP)9>tLU$jR-T20-6o-X7p}U8!$-@> zXI*6OenQ;col#_mO}KH`Ar2dNcXxM($O?DaI4qn-5^uC^8=ItU+n8$~$9bM%rAwD> z+qP}n|8L*-Puncqwr$s4Hj`0ViHJBeP8`Qx6BD*=%icD+@B4Y*@Bbw$hS;$h<|8xZ zx*29zPS?ZCvt(vwSj}o?$;>=c=61r&aoj|b#Xo%C`#v|FZQB${)m_yyv%9#vX08#D zyGuk)#>F3S=Rdfnnz=&^nY+8|IWyf=6Hp-hMQw5r$b0`Xp7H6;@O5uq1SpQbD~|EQ zKl~O{+yljR{9xfLXkj<+;k{Qf`R&h51mqiE4t2&a`R8H}M;DoZ3O8v;Kyk*u{pqj= zzPFlvIr~PVnVH@gEH;_NaUjzQh;dOMG$Lm+FfD zJNQ!F7wf+C#W&&Oz?TF3=`X84b>oS$e_o~`BYq*^J%B0qqc(SzBj5cCfqXX*kg&l^ z?wUo+l>|(QEptfpBL{uI=_>QVe(Mrwbdf3oiYc~k6eU%PA~|KHs01iR)gc}Iyn*nt zcvx9X6ee0+vz$PEPvW*6}A~ArLSM65iZ+#LOt=N33<_WQ)RYckWr>ucz6a#GSc#|?V z1yHsC0s|2;Pm11JC|5@@bbJE}X|uKivov|~;~CISAN8i$)*-%yX6%!@^~zVh97yRf z%ctzZriv4trX*WCQWC@vb`QTCWdH|`v0_Eqz#vl8at@`nkt>t0w3)kcI3)d;e030__!){N@+Y7v%nUD#|j$Oyw(&ecW zE?o|6)Vr5R0l(d}I`Cv!T#8Ql#J&2;r^eH**B; zYSIV(X*w0RI_0Q~3>+0Cn{!Gdlgu=`1#IE625zTA+i^-kY?oBA-9X2*ArNfJPaxy~qSLTYTddLi&aY~EL&~FUA)vO$-*L5g)_7nG z5BlQ2oK1!4i9*Sb@6;n@q}DUR%L+Ip?*eL-mzxQ|iEU{o152Fr!>~yul9zzA1bG&l z;1;?Q0;Y5*U2faALha(jZCY-I1h~%4M&5T{smGsu2fsM=q~E-^1UK&8sLdK6AbQD_ z`KDJLRKZug95}UE2dtTN@~Y*)bCei>x9Q|`=*m>ACVGom(gCEqcXGzlw@8hQFvti< zp5^GdMvBC0a)inN6NjLhkpt`l9H0$+2Q~uz_=8pQz&Qc;RTr{SFwYW)q98mhDokm7 zWlHwY&-#Nd;A@z>+%G|S>b^#KE-GL z%b@6T8B9OM@G(d=koZ2j#_I`TwS5;BvHOl8-1 z^TgIppgllpcGNGzbx`XlZ5a8E2YY!p&-cHOgBfvF7o%)EkuVN1~f&(5N z@E{;I4WrBu0}43x&jHlZ^q{k)9%$EV&$s8zD5^5|Ej?99F4>*9WOrhI6>F?PXQ(NW zvU+4-nN#~LsXtVoXEYYc7wpy-51(y^#c$r?2JM9lNxXh1kiiObvVaE_2bet!=&4%J zGto73xo2#XzYI16&~-V21XV>Uj&$=6U2YUP#EN z0Zm*Sc*02p0Eg(A!CUXxIeb>f6lUlFW=R0+al^xdKAh@0&aBS7SD0ZM=y~`vxU!?^ z!1mtVA=*Ja#l@HvLmY&~Myvy1+XNoKMob`?9lEF(+4Vfk5*%mAWua$BQ?^=*&=b}J z?WTahPLc;Qge1W8U=#HV?5#KVI4hFYn9H_S|4m?a#Af24BC0d2%HZq$C~OANY9Nsn4Yop zEevO?>nzO7Dq8~#j{)~Z0wTh>N%LeE9{|ED0WD2;ssWNXNJ4-c3gCe=E0%jxaApWh z0aNgi?W_Q1YM50bf~^tM4gdiIm-N73Y>|{m8XW}faV1&TjNlpo4g8QsyK|n;!cc1GaAOLqDu*q>) zMzT|ECj{(@GkPsh0tylaT3`i&mD)ADqt*h}?i3J?6EN^899twWcS#R+b<5y9GPvHr z`A9Xwa#9n8gCaRs=g=IsI^8z_Z}zxSdpz^-3nxoB`*pQnGGnKjz49{Q1exeEH3R6(SxC^e6z6s7jTj zA^|M}hs&w0Jlx}XTPASsAR6;Yc&&xUn{eJXR?ZyH9N`RHJaVpy0!1tUodsYZo?G@i zzsG6J-eFR`1M9N_h#Wpbx7D|2z((UoW>zlk$9vtK?!-_ zlI>y3L)?$Nm6`=&sjnC9YyM`ZJAbno^&(vb$%+M3V#gP76{4t_2)RR-b&^}b0!08L!@32| zuzRxu&hHVIQ2p`0mB1`0G(S> z>y&_N7&thR!0}uW$UqUClE#>uGszYnvLs1YQ=14|9T#SiyO?e|XoP))ZDfdO!yE#* zRnQ)LIi4yjYT}WJ1mWsF{FBypA&HDA4a+b-H!a=IjqeA|!-Z3A7a}ryndpw%umlC9 zx6-9OxMWNH_48;B;WbH%6S)E{YzkT=50YHAk|qEPFbzTtux`i!L>szj5S?R6T0;a! zmFc*%T0MhLmg3P7(t&dBnx6Y9hG==g5ebegY`o`&QUo-rU46~v8E@gt91t z5>A%l_zRqRz;su{K?x}ICXDQQJOdt#$wNHQ0x|NxnoeGVI?MUpfMGbx$juphz+gq=iz^&HeXqly$b z?~(&ZLb6?Q8%Q1*Y4QliGquMv)guF2`;i5JvdVhbW|@L?EY13EV=%gVcPv0k~8@oin@U%qpw`r^SiIwvkz321>44%Fbni!=6P6{ zsCR$apaPt4lqo^#F<=Xtzvf0#*^Ee{JAf6fc-boJOEY5xkpGkmI6onilS;A z+L>ru!5HZ}WN?6KWm|v{g4~6%q;Vb7$Q5#ssHx*2F|PA+M!+}o>sB6}m|z!iPxd6b%0a6{ z>Phw6I!lI*=p6Y+OC$hDNoY7!0sz6ZrYOv%9(Z+$$2U^7g+~~)v+>Db?|%R%`x{}> zZDv%Ex%o@8bd((Vj=Csw(}Hfw8CAv7aGw~dQ&p^xGD@2S09+CTH}TADHvw&u9LNPq z1+XU@G|zLgH#HAAA!m-~mP6%bN&+rAbypjAcCelf+R27)qFM2*xI*5^cLOi@q&bq9%0BCh9TaN|3jiOu{k&>!OswP(ul0 zV~PbI?)NQ{$N?awJy3n=k((8TPV7jaP!;s!xL)5eg5R?D_TMKjs1-mbB<)T}$ zNnX)*Mp;P=4n-GtX|an^`FRr$TOb{g6Ydtax&YTCsjCvYM&+zBE(>UzS6kZVwd@u9 z`kUe;S9h#G8V#iSvpH9q{Yb<1D6MChCMh!w6Db9Xn1?L;We*29`0cW&Qrm{=qox#F zNFs^AA>+VHN#u}7xd9*_#@sPU*e~JK37fkx5LgRL;geLbp^TXTHK`6orj!hb1%!hlL7YGlC$}gTQY(wf##^c*ePFkM+yuoC zC1fVv-hu*U3mEdYO`7^i%IgmUzzGHo4+c=X8U$Rk*~Q6N_F+j7kfV}e-ZFuUR%Y9( zd8?&;Nkz=~Rrvvrg8~gQk{%G$$X^zWaw0L@6<<7{500_YpdaPj&&ws;Qkoh1&O{Kv zV1zelT5u?nHkB>DydbDceDjNFo)Ho)QZ0umFs1L96g}z21~zDE-C>wCET#@jQ^y6F z*&4KrY&^;B11OaM1!7-^WBcJ zK45|`@d!>5#5sP!!W^pe2j7T)dxC1(!P_EWG+mLi0rNit~&dYROg6S0yMlh}E8 z0TaVEkem^KwFGt-%2wlWbUwqVc4mdq6R5+!QVy1ZNhl)a=z7lD{|i&@88q)y#MNBc z#aPkGj+XXe^`rNyAO4Tf;FIEmSLo#si%v^2Qp(!UmdSG$u8(bs&mk^yU0sp@C4g0S zl>nrV3!*&9V68Zqb6BQC?Q~5HEoo&}c<{J}!T+io{ENo+y<&QX;Un{lC8|Q7S2d@@ zA#O^eO~WOMrI$;>-g3fwUvB-`-By0~7Kw|;Ob2YI;}kS^4!{E-2S%zZzzVgNeW*Fd zTw$Q5Idw*hPdT19f1ABL=eu0B_kO2C+^~UC5G?K&h5TO4qld-Xmn55gMp@zNh=QS_ ziI7MewE)aQB{n1_&EPTiR<$_oo@kPqX@x{ zN8rqv{nVIZ!4$DypbW&RDg{ydnUau+z}`x+d&2TTv~v4e-5h8a=-^KC z8{a|l%0T3b`CYu1h_yFn|qGoDXsIr2WIRo zw0Ma@ZIn>tkT85I?pH8Ql&`Ms>giAkAPvCY%km?X7M%Hz5U4i?CM1UlW}2Rxg7#2> zX<%!Q{lq3GtZ5?zF*?s+*-?a8I4e5?5{l??J+2ws-68Lu)Yqg#(&^fcdZ79OMVmNl zJ8(>LfU7ZB>FFDW;SvC0^6Cbu!4ZnjP`QDv`vi*x7ib)~QQ4>)D^ADxqeL!X0uUrrCa9)lJhU+~l zc{kN(Fo+Q3UlRqCz-1W5X&@-{uc~!LAnu^B!TCUmye902#Pt%QPHMIWpPG9ADD-fp z#K6%709+0c7am`5_Is5aXI4~6@Bl@&r_nQf%`sA~XfX@`;)M#ebFSmc{)uc1AABn9 zSn!%AIc%IEjiZ{p*=}cdQnW?!mK``@(W35!4B)cz#nAB3T+OMOdVwb3o{+Q`U(FZs z=`w;)XizF_^6Dl5?xy&>Hj_AUCiiS$Cv@j-(MAb=E(z*p9=14u1oKKbKdo6G!(>b; zN)zefVE@s1qwq8^;4Dm=+1y+&7tmO{a#^el2-_dc?glTj93sI?pKqcbz;apeO;8f{ z0{Nk)6gDlihs(|MKp?2F&lqVAnShF;+j|$ED?fMW5ol|JB5AEyx7-2$6%v{WE3~m7 zFE<&cFXLfbw)@o=+niU=Eecx%bsF5ZHFnldI@K-)9fgWAU?$TAr0Dv<|NrxfY=cL2 zr(wMtmV#^GTY|g-QHfjZ-~#F*K6=^+*;VK_{bp)O64v!a1$m(vPzfN{p6UYhyeJDB zHD;PyDh3#UsWLEu>Xm94;N`1pSN@AHQA(4abhqvOZyHJna+60OB{LFfd=ti+p<_uM(BsIGxf(k@4^dIn*b~;{xEiVy2bnPlY8VI&hc{5qmbUbkpid)JS$CP-|Dxe?hNoEcf=Ba z+{xQ*^3Bf*)#{{kpiBCf9_=~0jhE%x`#}K!m~B#-Y3NKpk9`{vpH$7MmqbXa!e)xL zRoGNV_`*F${L+1*>m;sgHhK_zoagZw!jvV+pg86kQ0-}oTr3(Nwz9R9v?&6vqIN4Vf)*l#ko}5f?IZV!SNr7e)NI zru?ube_V)9bERofg2KS-O@VqV)zx8m6skfqE9L9baF%AAGOZV*p#)|_z^0bq^j5d| zluZW*IHan%3Y#xc)x8cLgsgd?#I{I!S*fYb3cgSGzJFac4NR#Qo>EO8bH$3Mh0=^| zxlup2#_EOumvKqambM~#2c+1XhKD3{K6qM+D2bjI5^kTQ#Sy|pxQCY(6|`epq6LEm z&4;3K6>&&v5XN-dOq6Tj&$#qodaOD=DD0;tbQOyeOWLhP9aN%_xPUlOLUa2OvF3SN z9Lq)Bl&wvgl%2(tD6*z#z`X!Kw2kyFf|3x z4=@^gJeQ86m00^!mUeDp6(Y+(2tyPeEU%xI(cwP;v~cVx{}8Mn{|74^^8* zb9QPFiN=TpBbLl1Oq39X(-M-oK3S_*){~64RYmTt8RQmNXbqHmXt)|pI)Tb zEvHy5PLY#WK$PI3zrX*4EMRUWfGb#G@}i{w+n8EO>W36kiV7Gusn=BGJB$=o3!iQh z^tNpvkn@T(nQzg&qawT`u{{c%6FRUcpx6c#ObKudtUj#)%5+dcd%Th;Z$S|eAeYNW za)U}8=Jx}s z)O^ui&Hw{ch)MwQamy!npmk117{nqhcwgt!=Qs|`hs2z!Dd-1EVbUOp6)>$-=RP9B zij)U4C3`zhid1qRl`~A!EUFR)alyzV5V=T%P;)|lY8?2gr3{zZx@dY@2+EY^3Q7{< zomIB(%B%~*b#$5A_ikef9oJnjZ zfsq78T@v#kR02Q-CffqDixdGRZKEV4ue3`lougHp){7y^h= zcT=vl#mAxkTs0>)>Bbxj=V3@Z5CEwCT-Eq~yBLw;A>FO5p2i4?%Y*|4#VEwUwerCz zH1+-0fW0LifR8Igmok^);4aITS!wg4hlN#gJFm~m4V;I)q%A+}Xy+LktCZXio7^p) zA%&cB?RN3G)jDOQUI$Yjm}xpU7_!xjy<$ym6P2|WRcP*vDb1Klex66PC3?+f#HNdw zK85vmZ2G>$lVo1>U3_f6*!+a(y^XmHhHLgu%|ans6sm7T{;I+0+z0>nUzvv5~kd4){=>jIzrS4Z-DGbLJjT=8I?q5 zGpBb~ILJ1{JM%ZT;=vT!nITAr8E+;vqhryg$@4(`b*q!@Xm1G!houaKhJpPWBP3N~ zmJQAhPVcTs?U)R~(2?!s(Q~UGO}8#8Xb@n~_9N-)jfFqi8k21*0c#+%xsZhrob>aV z8d<)uf!9HoFrd6rl2R=1z##%6>k)#gBwhYg88l}vw2&k?&@W;3gNW1uN&pQRm>aj{ zU21XTT*GL%2#_saV*tl$&sg(?V5$Qcb;Xgi0w^L$kPwy(vJ$9%Vw;pXXM_7Toa&7bIridfEpqf2iX~C@3N7ApsuDDO(&>sWCs^292dfcy-BW65v4B5uIbEO+|*gWOgi} zz5Rlf()Bzs*gZh{gd}3H?`UpJ)07EoSr9w`goJ)rL`kK|E2P(<0=k0{G-xOD$Zbn_ zhYf%!HEYF1?Wqu-K+3n6sYgLW5>ZdftVIbDglu=QbTSzizisDwpIYR~x@EERXsvePT|_0b>_bYEPO z;BbLsnu|!J$U4q6xNNbjm9dD;>N#JvB*Sq29gC<7#bR=q#*w%`L-#R5r#b+{+$3rG z6onb5fF;4d&{eLDB&_atrUyW);Xo(s?@=VExhicbRj|oOSnIYF-nYSRTv3#70N+W0 zngC1KqLl#~JWy=9Eu(W=1!qvnQd}atHrauVFHn15OMroKtc^hKaNx0aMv<^mXMcEg zg$n=_E}#S z1wv4BQ`nDFVxsZ7+Xf&XW~J41YtMEz0*l?9AOQ?(>uaH75Sqrt)C+~fNSo*Du2uib zR?yu}A^(YCtKWsX9cX2;FcS-3(f9_%u5#Oif>aC%tsUQ@gq6}BV!g@TprE>2+oFB%VnZ7?@Rm=DY))AYy@yZXz&CN)++N>dPdlZip*Osl|7|tO~2UXzu{vK}O7O zaq;aHq7c*|phXl29B@FOEJd33>45)z50-(6T0}{T2ttG4Rq`0bKuepXYet>U3=mwC zm#wqM=~TRqXP_cKoJjt zIuF}c^QL32SL9JT|LHC-$#5BQyNDmgcQlcik0xbUsGNz~l3U0&pS*QFPZ`L>t zfCQW@f+MQf=sbeq%qSu`YI2XS3hsSFg?M1H$)N|GsBBOf5Q;fHkZt3pw@|a9k|CyY zh%`3*5J?U>T+q1_a2W^qw>$0kU+-h|6(eklBtvrIi>m^Qg(V7TO+z!e3E=et5b?O~ z(v!`hiuzeDozTR&M$>^gs){#nW4=Msv0SMDRt!d^$>Y_BNM@=)N`cfQMY6RaFaoUO z`S|nH!Lgf{u#GIq3ENn#*%}9knhX&^7sNz88Tl%i0k#qZ*Nh>WW!eMVPWI>DY2!OC zd-EkYf@6@e9XQ~%MmwTG+vIDslvJXGlHyICdWk2`qs&NKLJS(Qa;Nli?DOt4-3^OG zYz$FbslRjP5PyH3HYIQ%qX1;!RQq z2b#0^%0UyM(oJD#5f>!OE`qedx#M$GAGhbH4so_mfgci7lonz-3IM#NZ9$JJ!;5 zC_!o=@4VUQKj+r{@06Sc*nPBt^5rz_2#kO1bt^t)ec!v|i1W0KKa6k)_u*Ay; zh{XN8j#k#fzQ$76^xo@vlFkuA7gKYNnVf?7g1lj8wH2&6@BbYcq6m`qr_DBv5$!^- zB*ma09>kCsL99v58Nzh)&#gUbG1L%*2WagA5g?z50#+x^%m`3Kz@a59Cty;WX87Sl zNBr{V>Ak$&mWx&0HiXVovNO-H8*vE69vgLX6Vf7l6omKPSWPbEwW2WNX43rI80TWUHO{%M;#3yBGsU$#4}{_4B!ShKPp*L zAyZ-kzu6@)+^&MXe(M2{|GqL*hOSqT?P z66RI|LBO>kyZayoC(*DDB9TdTaTXLRMKhEjB>-62Em#1t5DKCY3n(&znK31m3qX6~sgw6fBXQu5O;00`hph z&rJwR3+VMXASe{;i4%4yq&&bViC0t-4j`nE?NTV}l5uKhF6(Fm04rSrkcEZpHoJUF zL!`5ScFZ)>4!5(~HY^NgcTr0*l8PVJ(r z$)WFB4pfOo2!;p(K|v*Jz}Y#$4mH+PCJ-C|AmyN9JN&31Dp7&}E=tbHsF2;(J2Xv+ zg-O^V0OZWP37E+^7T2DP0N%V(XMQRZ?ORYU$sCx%kd(^=I^S`4b@lVr zyjsG^vBqR-ol8^XaNra@2}2_6WrC}M-3J4^oI(bRz9ScuFF-b(=p;FpRw~-o6H=-O z8!xKmL|AKs0zm2Sm4zwM#N!&SQ9DoC@c7ExUO{bD^$QMmTjG7@`VnImhr5h$08q{ZcAtHbX_rOfKbMUvq0iCWJ zu+G~975n-C@p;2swcR2~kj|V^2TF#?ZuXvpDHKz(7!y!LKfr|=hDVJFm)6we$?|Ku z`qiB&meZ&4ve$`3-$?M`4k0ijL(q}1QZt6$z&xavT$~9Y_aV zm_>qw&X_5y5(0f9L54v}yL})4l}dKIjVur2Wm3{?*(?xVeg zq)j%JG$>qNBIU>;Axu?~!P1gyt;80uf&jHeML$cT1ULc^F&o;2lm^CS(aVUBz)5x# z9T&973-wcoj#5b=)h7Vh{@HCRFI{1nPH<;ot+`!T6>~nSRiLX#_Yr-70&Sf_F=y&w z5f^7v;1mPOJQMH?bGTtU%^~K&Q*Sh6@AFX4uowvd;8=-3cAxljPppq*cz-5$8Zjr| zCF?u13vf~1Z}`B-rp;Ysc3vukSOAQ&78|@`l>omIMLzjvGs{D?=1cgn-G9&+D>`5> zT^BrydL>{yWDKrjWkI?`2~ol2P}vgnQKc)Obc2I`N(;&fq`?fF5IF+IasyVY<_m>c z2vD>G5J3ANfS+19zC!gmQ+qkJ{m~8qSAOOeTy4Xiu}p>%07xM+6lnpjOZf*>p~9Q2 z7Kc;VOS-Gz?0&xaZQ|v5GXaW4l!6qB!F@`xolxS?$WGKfD$aovwk&-k+b;B*C>9M- zR2FZAi)h;6oJ+2Z(NF;g7*Ib0K&D+9E!Xh?KoE!phKguFYwysY#Bc`*b9?$(OP6yw z3?K-^cBw_}h?&k`0@%UQMd2gBHabv+h6~b`3OI-@v_l9$KKsD@UKH8y#6lK$sa!Eq zbfl6Br)JR=#td;${{t-pKF4{fqDjD-l9ETkB$5NqsJ1T@<|;3}LtXiMJVrAEq^X9S zn~Y6Fp9D}~OoDfZq=C$9lXhS0o09#2%R{&*^W~Fa@s?2BrA=>6D9tf{Ar3GsQb|a& z)#L&qnuBH0g6N0YwjkpuG13P6vPJQMwT6Cug7mHdcLgKawh#e~S-$7x64jpvfPy|X4Hw_dDAEv`bhC+~VxghNfnjVEx zP12^-L|}f0n)q{xc!}H+^ay_jGcqm``%>}(OZDVGFL}dZ08;kBM<`hwXaiu!oZ6Db zoD5(zBfZ`Cm|2)GFj!Wn6+p3~DaC?vQ*gEf(&7r1eBeSX){OR)JBK>-CtI@m{{@gv zNFom4gol`v!328FEE8`?8;N>lph0NbF})M+P7#!7;8xRfYMa#`fRjXOiLqCzJ9jFhTQfq0J1zprv{U zJS2I;l9#q^=Y=o29ybK^BDeKVV3K4*LZs!+i8%D@fyZK+((0tq_t*$@2p*t>m01WP z>zO<-J*$1}+%42^*~JLzC`AR9$TS-I+ufhVpnf3$1GA!wh0#{Zm9Qh?6KNWcw)F-r z*}1i~4b*T2qHVA$zXW9yizd48xMaIyYXU)J8IULdA9C?{%R4@nHJ5wE5;{)`rT1YlZQa}&__Y8TX90G5O@!+$P64WyD`%k=g2^=1B4FaYw8 z#!bR{0k>IE(hK!u))?Pov;E65k!BJ$n4RSXx&;xqmi@I>n*%5^0yz@RFrbqno5?g} zUwijCp>v<-5VLKmkJo43ZjHoAaY2{_vE*ZBQiA|=k_@lRv8ed5jtqbU$vK4fpjAb3 z6iN29`8jAj2SYapBaecA!K7632BY|Th!39?U;UuU;gOzJLtT!^8d@`CU(KH|#Dj}> zT^uf6Qm9PwGH4+pSCg5FQsvUCLaQIZv_Q3`*&o*2zK@vS)v_CEPNm7)p?Okp2P%>t zkz4p1AjErVT2Su`Kg_|;zphRYW1^o z&b1{A#eoD=4}Eie3@jmN9|QoNKuT&UH{t~2fbpf&rP-Rg&{<#wrn$Jm&B`_Xvjf-hgLF;a zJh=g1{}Xkmi()Ro13X-!mwk;ZAHxR=%dH2yJHg0$=DLhSb`p63w%wjXv&le&Sg2$O zbTeFuE39ou3{Ny1d7secN94-m9h75hC984!6-fM815PrV5ca7Zq^q5kl{ z3D-|jInT%KxQ#h2_mp(hZ=v!Pp&rz!I+68sG8R=RIqib6J73j&g30j>Fdx0u7M#gOXX}32r!aZ6pg-!r;t_P>qZ5Ni{^en z7?>wBTG|Y2#mHmDHTl1bff_hfUSx$ zb2=my#eum(wlxor>BXpuX)<3!zlSJA*Php;e-raV0^Dal6-|5sT)A(<(S0OP1N4wn zUT-#Pi4fwIc%6ZgX(V6-sk5%~J`;u^!xS>@0uNe;-h2l4I^=|W{$H=l%>EmaY0c0S z&0u!bl}ag126>Ln1FShl%e4V~&1UkOzCu_$s5+s&b8^t>j5;I5ME@&+qlxgd011?& zme9hMX0|wNXyh1tSjE&l14c-&=>V~o;W8K<>GP^1m-YIRhWtnIc-ppuBGA~;JwA8` z$>BCqSGQZJ+y^OWNv@~-5Pdi%?4PY zBF8b(rg2eHK|C?_N})vxErw*!)Zt3fRSyu~x9~-=T{{^si^uZ@0mxN1=}4Z?FeLy~ z;E_}cazbU>Ww+hYcG@E6CA1>^_M{vWAS-R$Waj5)rhL zFwhj9mhfYbn!-4$*T6UKG(wHNnryWg9i8Z&Fj-4q?c>ky!D=C$JmgnR8V2BiiU4NN zD{NM7bN-+~$}w;d%M>b24LFK};2*7_ z@WcvFtU!COG}m6t042az3>=NcThgvw(9*1?wN+xoWCU6J-)=SI_>GDXeH37Vu+6dn z(}CX89ht^$sHm@xYymIE!ZcB{tZZfJXck9%Sdf4SMx{;f?l>kP0SG{WCQ|J6g*4&v zhL(x+0Ahex06?_2THldjzi8lIj1l^!4X~{C|JPqC+|LOmnky5jqD)4~#z0FMj%v#W;lL^I z0=#?~%~5vLqr_H~jZoUgTe`goK1MOQKeyk%xx>Ra$D%9_L@>aG1*!Ga7#D@O5tWo& zwrsE7yo>~9jFK8@*#@f1X#T>D@9L;48v=kd^BB_0xPcwr%fl*<{Ub_y;tCsq7{GwE zZQE&G&=PR3nV@Rrnw8e9YP}2eLZQKJkQx*cK*Jeoz5u=FyE}^~8edR+WKokT{<|Lo zI+yw{F#5m{;Q;6}ibMsr5a5#fleby>UmrvXxm6>r!lcy~Fg|Rr6eEU~<`5h>`4yT| z*p%wWru$NWVgja7m8+|ah6@gV;-&*qmQAl|iY)IP+ zlNpj>CGTi5_E>7g41>;~Xd{W_EnW6ObdzZ>cjkhjG9DqcOVdv`$v#}DVfbVQNg*&& zl1_6spp-QJD%jKO!ler6O3Nknq!EucI!>Y@l$z!r_B>lG*%g#8U6>X3WX$B@O_wpf`QvAPL0ytdn{ z24KhWNOMPyBcy?p0GVy=%}vX>>sdxt3^@SllS5+5$4Ec3QQRwyg~YNw2`FaHKdRKh zNn?+aTa0=TvhffuYRKXSm~ICbOi8&m|F~nH2ycW)eDlF_r_QTsKo6h=!n@FX(Rd}x zAW=kEA9%=Sv)kv=>d%DoB^$OgxAKW?#GNXLIb2^i<$`M(MWhtRZ#j*Gem6VKbm;`} z4{i7=a3Inc2{LM+l8j$7S&@S80t~y}^nMLnIuDQyE)F6Bc7xokbm5CHr$hm|O)5-Q zP?|hqai%K0*dlefv4Sto1?#2M^`R4T%Pxwk-Ax*}tgj1zOC^#+Nm+y@Gjfa6Rus!k zuQ%<&gTj&4jg=WB)>64>IS)n>+=FIueG;)4%V@qRtS3Q4qmJ3i z&!W|y0{-QON&fq8?%THS$=tz}u69%Wo)fnPm5#H37PM}xZK%}h@=|OPL@20=@)1-n z1||zNY3s`v$f>F%Qi3Fm{wL7JOX$WTDFH~y32QX)t6OQLy#Y$t7gr3ugw8a~IBg;# zCT$Q*z!?}Z^9=}U>Fj70YYM1E@E}=de{Rvi>H`=7fFM1krA-C5xSqxtx<+Ng(Esmi zBs_o>8`pLx_^O-jt$>m+I_z2&*mXM*b+QeZL^K(?k(z<#D3qXcEnkw!x7qX)kfJ5+ z^_|7Aqay_s^D~RVtug=lk4@WTa`5vvrwoY-GEnrp=((FP;aYY*^HDO7_~n0 zY?A@aOw0?Th@-pX0Jwx3MD@+ddwav2Qm_{-TC|ItR|r`SPC=2@l>?S;)>y#2UQm-T z1Ihrp&a6>8w{F2uv1|`^=Xy0Xx!C0~i#q+i>m3_$4{quLKxaTYu}kN}u&^6LG-L$= z3zoW!$uVJ(AYY0CHPBD21Vh7kJ-b|1zgK>)pAo=Y%;(xBs`!sb&1twgZnh3-g2e=ZMX(TKMRNlXY#}iW|F<)|ccvmZl(X9fkj$`l z10)HftE~oL3qV0;2B?=xA1IpoehF<`Mrz`Wn+Ozo3V?;xI~hs?mb6whMg}=E`~4pJ z&0fm`5KHI@P@y~>q~H?R=>{f7(=2;AwJw-ZWu3KCS__atWc8qxBwsu#I5E-*(@|Fn z1jI#a$)*Qwbeo*q&;OR#KG`&6${q4PyrZs$W`e2`atG_>deU6gDE3;Q#o=VrmN(fs zF?~H9XaGqQDuyt6wF|YNMWSJY}ZTy@>myKz;{lD{-G$d09yzE=G2gB(77); z(plk+wTzM^0rVSc=*G@b?fzag&0GoV%r zSnU&5zx4lme=>InKnPla-erAu*=q8SpqNhpO?1m&mDDoHCcBw6Et8HN`sLDFnyXZ3 zPhq_Q?6`_tc?LvX4!UT4!NDUm?uXO7`gg4TFVF>Z+6mg=4qEZ4-6l#+z%W8Dp7MTd zM}Q4$?Za@YBZPg{UOVQXL0uvnaZFTIJtC z@J65t*rGY-5FrU96;!vIDYz6dJ0%Jz1p55UA%Xm)O=Ex(x|K{4I0B5Ubh{=6)FEm) zWF-h-q41D)XR1h(fRwx;_8i(8tt+xPTS$^o0t%?J)&NbO5LXLyoLn+4s}%V@-JsR_0butpvveiiH=~ya^3gCbbcb zK#`^a?e{PZIV(zqOmU)`W6Pi6FCBi?At7clX;RvR8+#fg9tT}8O%oBq28Og)Hf2PD zcFXt8P^8MhpL4%4IMcYlXwleFao{W^P=AEhr46H$FlXJGcwglx;-*t$3`^Svu|>8@ z>~X+nkfsgoJ_bi6>H*C1i*m5G+I(1vzU5u`hZZ^jHAlM<0>3~Nm~ao>2(@f1mCm~g z@yIaKFFx_{-Jah=ggI86#uT#jrJcNETa246a)QTcFqrgRuglt6LPlZKKI~tVcMX+i62N7{H7{4@^H) z!ryC5*}>R4Mva5;M7NtP-q-eZS^V{^bV~tu%O*Tlzy@;U3SuF=3U}cv;h6mEmMv); z0wAmQLUxM-uXaWdMLd>aVR}Gh34FF{K_%c78G@*xMHCSj0eR}elZ7)gD1|0G!FK<) zRIxdl>rEJ9s1W&}%;kh6>pv@mHibnoouE0eLXj5yNbr(8c?(2B+wK_kvsMSXx;7DP z0JU?VtE&sHb}%KLYCSyDHid!p0X+qWUcL%1c5(#B6Pl-Nueom9!lp5dYOIXo2&AM< z5*e(NP=}b=W!z-4)J{qtl zv`$C{kl+A%jLV2EfZ4xUKuaCq1Mhae=?VlZ>K!LY6mH%vCf+?XM-otlGS~Aqc6Zkya3w_^xbFC_nt+bHe(zhG6%XjqdajcPsn72hk}Q8>BBA9}vB0{P(Y8zqU&` z(b?AAZfPkHtQ=BJz_U;VHbuaIco;E2wjb4dKHweYOM}zDxrmfreF>K z*xW3VH_0SHB!Ed0Kz~0J@m;cEgKk=WIe$opZB3KJKVTWh~0H{L71rPB3#Cy!> zsMVZU!&ymSHK5t0p>)!e-wSY|9?)DEKsH1Q8EytfZP{KmWKxsjCQN(kW%1wmQMm%m zAPOd#k#Y72ClQfiA{k}?NoZ`WQ8iwZN5BOOT$V+`Vc}duG6OlNY3H4f++h`yK1e&h zvvMfXg?Iw^<3RbP&q0sS@ZLrmf+7uh4N2^0RtT7>$xsW#M9Oc5EQ;eE1D>HNh00dL zQb&uU%fS%K&b$u|G7`z5mRqd3AStG$2m%C8(n^O%o&wXxbih+7*%b=1^N5Epx^l%v zRm<93LpK2tKgrli>9qK|^gg0jC%(7FlnNBlRB0huiuL4tGM|776hl#i!q^vDU{u;) zpOmf`n9!uvu`wZIdKi^3qDx`YWEI55!qo4C6+cGfWbX`xIobBU^qc?w>pw{KpZ|Z{ zDOa&>dwyr-Tj@9ecLI=%M3Ha?gOHp1jpJR#RXDB^76?-XJ3S(F&DfJeJS;z+1KiyN zcgNNgyR((mB=YBpDY4HG$g*V`c{DMZZLf#{Y$CR&G={Q85)ic&2v)4n`^Do5Q<0o9 zHU5J?LVuTK=vdVI1N|!_f02~?L(1XNs7eY{+%XH9B)p`cKsE4?lqP72j)@fHpfxnQ zuzc-g6jF|z=BBI6}}JSI3?=aSO=Ir_zLkROC;zYPPW zRi6{eHabGrWn^GUYCB;7W(cb0Ga_UDs5%&0!AuF^uQC-Nl5^u* zpud`0)q4ZI7nGeME!sWmhQ`DbO>Y)zsU)oSvDJEL?|*B5I=Vv&s2=8tuv z3Ry1lgpIB;NkDRdz!kRox1{=|9UB3tsk5)Mk3AF)`4eH|YlwHYpz3h9%CkXq=O0mc zx6nzJfiysVQz!=;2qFlRnSP)GSVDj&R&)WoUGHVmjroI$M7N=iy2ux!%EZQ`W(g7G5bMRC!F z|M7sk0kQ~7y*^ENuT(r5Jc9&Q0~?3hf3;12#X>g*Qh@{hCD{F&2foFQ)Z{Z%sfYwM zJroiGgd#;-d&3(c4@iF4AXIJ;c%iOqr?R$fdxK(KwXffj$a(rHT~3NdVYhb-J8Vb( z3Oe5oC#R5`RzQ0o57ZWsH-)l;*((%{92FFxDAHy|E|~z05VUiJv}Hk!G%@HXp=H&5 z;CU$YTc*`tu#i!);Rau|Z11yZY|{Q{BK<&dXbh=@s@i6`TVTiqP!kl@yil{(1YF3X+CamJYQMiLYy5c^Iva=yR8o>d4tB@= zSzGGziAe=Avr(9o0a^+T(~+ejYh#qPtf;CQpf(`=1&G3<-ZkAZh8-BH5xuW!rDYOU z^7T0f|FP*NWG<^x-LAjzdP`Z$!0p>kKM z#+}h^IIZihINjfUtN7#$d!QA0Vpy2YFB$*1eOKYTK54nDn)y!VX>aT_^~B7kcO@b_ zOf%9LmM4(6%5Kh{6_E923dp1Zz(T1gf#iNQ`TyXM{|zf|1i8(HkWAePfKjyRoaRpsHXui@SjA%u?C#XpA6ck?(8UP8TkK{sM|5#7zO% z5j>=#hY|B)`+=P(l1tS?{*bbD(uXwS4B4HgQNxrwZ*e-m`*!PIe>-h*Mw^}Iz?)+< zLysOi^0&WWFCTtJ)qw?m=-fVy zf(;aNf(y_t@j~?~UzG-V8Y?oO9e~A-4j7#(=LQvXH?O0bmCDp-9S9Ex2*~Has*nyd z3kpb5?3WG5CL*4~Wni+_A;WIeDAC4tJa;%LsnyW5Q*3xjrfgH=7_=n+AdFlKD_k}= z@hDX+nNoyG1~LUDz*}tGiqX(<1qV>zf>JMyv{b-LIA5-0Z6eB+tw7-6 zd$1yfS%lv+Kzl%!C5HLT9=k$3E>uJ|XCG_`4FiS~j6E-2F|M!=!%*xMwIi&I3YfHP zC^lVTQ>-ZoIU1rCsTh!@6$vk<;n2uTcUgf-OyrnFDuIPAEv$vEZXFU0^amR#4^aq2 zgw{Z$F|uHEgHmt-fC7aG##~tJ-sm#?es41)qx65z?QNj+cFP;Rs&>vdcWH4Lx?u#puoqq*<@PcglO zX$`UvJqWoxkNp?4L)(-CQ4GNY3Ms&X3M^|ICZHj^a7>HTFsE1-hHNFJ012WAs=)_c zf!9VQEWiNtfTYqd=Yk#dlyBY~KoZdD2POccY(yzVuvW|3c2YRZikfjW)G#1CoO;DT z6*K0&F2`}4-hifnm4*!gp~=%j3t(skOj|h69#E1S&qx3>;bvW+2d{Qaz=Qy$bn|(@ zWz@n@D5LP07Jfw<|i(7IR19`r0<+(GGbuFdYO6 zh3&U@`*{gv1;z#4@-($59+W=&_{YH5N2HNQgKr6xCMhM@FO;PiU1;8;n&WBHyTbJ2 zpmfDfmeD{}>Bgv`jTHuw>lFefQ<^N$fs>$MWdUyZ#EgQQ3e42+VQLO)fcEcRznZzE zDZtZPIV5m6Jb(i~Jz;7J*wW4Y@_!f&VuJsV# zOHI2?d|!U5LI6e76d!3KR;jdL#~p>B1^`5a>Dt@OlMNgIE$M`p@N79Gga_ep09r7V zX&G?l1_5f^*cyg{gW;&57{E$doni4)WU?^QFV-PT5~9=o!;5A1>))<^!DHMW1^~HS z4%-$@PAUjl@-23|dL`}YlQ#UymDiDT!PCjx!&4HHE;+sOY*Bs+;bkpF1ojLF?Ey zhQcYtMW;)XcqB-RN2iu%lwoD;2;UHfUpex$9S2fHz)%25Hm!xHPu)Qv!vX9meAt4i zaM(+qXC zW~GGa96#@y8fqDB8)K_1laE2e`UFa*>zH~4((?J~1VnOuwE!+dD_oi3vvw!#Fxpi&-;mi1dIDQ}RGmM(9MVeeoqg z0uIpE%n>MAwX1Gb3US!yHH=|rM`4n2W!cx+j~$o614}k^{C2D>|40vjGkmVSHhA<%Unwc8e%_X4Fk$ zp4cmnp%z1+)|}_cIxbiu=`dOv;IYqPTxXJl0^@1{s8h9o9x!9dMS8RXfXo$47sx_v zA>V`SNx+nO!EnPy$axt=0W93oN5#~v4A{GZQ9C61eq$!L(OSv9vxs775;smVoWo9{ zgF>)k;7Zk|*=ug#BDIMY(QIKGDT*nIw8G6wKnN)X2LRy!Muf9Z2aL8z^aiSCVe|lM zm|k;GYQw{k2%7r-YlMcm8NC^ReZday>2Tu^^Z>*8g@J=s=83_T+31610099Z3!U44 z7y$_(AoKDAXN5FXjKFiRXJAd1O~Tz)_G!mh_6{%)Om5Wa9Jo#)je&VK@V*BF!`u~2 zVKEPNh?{`DX+BUG%mX)yYgeS^xkvp5RH-$nfg4c8P%tI&4{ZfF-e7tLCb8(*byUt{ zzrYmYWpDwq*M=0Y0XXR324<#UU`m3SPhF}7Mn41UHp9%!PXyM(3~H{0sF(tnQZ>5d z9!Zcz!Hsly!LheQ)-eWURtHRLP=k@PUI6~XE>=#7M(mt()NV#8(@aXAy8l^v2h?)!M^Bk`HqLLIQNnuOYULr zUzi#gHd8&Gsei&X5L~uY2rwSYi4XXWcW459^5Y>MvgiIDjPStdO=0dq6;K6OMo$O+ zcfwwCpnk@nS87cGn_+}keUI%Y7#79Q<1n+=`s;Yw$k{Ez}hS?O1?P@iX zRIYgYjUUHoWaFYUM*hbmDe=Hjndt}6swjZ=8R#--(H&;Ca8#ubtLdlp3(FJpmq})u zK|36P7t5)IB@;OV%t*Sjd&>WVB32~7PZCSsi{ z-}MIxO`|E#3gsXZkf$?!8~NjZ|CM%7DgtS$hGwtv8J2-hD_}(?96zIApnPF7^z2A6 zKoU+dv^?{L^<|!LF!r~3+cCgRkrH~akty)LZe!KsS zr}xRp9cQb>hjQOjJNvb)x+FN@4w*okhfMM|$?xki!_9i9%}ggpzYWH8uz?f>cqW-b zV&7M^rdJ)c=g;()>?6mmJ!@;+Zj%@A%;levB|L!NS zdyWXbDm$p`_vC~cq>v0@!fBtl>Ff}uoRT3YIutk10EDNf>vb=}WS}Z5#FZ(|0x$t| z^eAo3qG?a#Nt;gzdZ0DS^mf5WS`>HK49c!K!cOhEIP^`(oCFfFw6%3~(a3NhLA)g# zvUWjD66FR|!*76lz!tubiE{wx$eC8<;NLn9z5diD1s9PJBWA(bnA)S9eYLK=aI;2v zYxF(WjQZ(+E6WfYXs<0XGtR*CSAfanvz}o*D{59Tz?2=$skO|6|F9|cxiBZr!#5Oe zP*Au5?%_t~S8p5kYM{0(QEzDF*gv_hwoZ4iZ;LatH=Civ>^BdyXDlHtUl9qY_EEei zL^t6-9<9Ie_+~cCj9~!G3|6Cy3ef?toT*HEgzYZGYYcvr);(W- z=W$;gq@@B-MoeL>!7x=bK-G+Uz%T<8Fci!-i4jAA8}JEG11zyCZU7Wa6&qKhs(J$`7}a|)0~D*dLG=<$ z1DGjFHM&=(hACq@1{~q+AP;Kh;LRK5#Ggie#;NAw$JO_@{>Wgxe){75@z37fHK0I! zw!r%F*CzKT-?FWRCGKx-~}?UMKekS4j; z*-1bkF$YS+V4w=j0|Z_M2kwFAIlzUXpPwcm04Z!;vX}*s2s;LuXFbHu&v~yoW8@9R z^bQXWI$a++{_*ky969az z_x4oZc~{pjzi>I;2u(3@sfT{cGhj1<`*_AnDCfYf@-DYG_s`&V0{{Vh$o~Vo@m7jC zREiPY*F3v@cB_WI%huaxAW(#GmlV5Y{)2?%sLlojbqL5iIU#vUr;k7jw{d6dVs4wiG@8_?44UTs-mG*zpFv290;o?R^kt#;8=Ga3H> z2Xc3JM^o?afEWJ4U-)4kmd~h{6&GBHUuMZ&;aai7Cw3}!*Z%ffQgHyq@5PrLP*^uVCwdV6mVxVvjg_3>d3d)UMNjSG2Li|=@=pFR6)5|9WepXd{PqK|3x z-5vk@pFe*57Z-On^gTp)-=Ad4m*5ono-FKrabIUb+?7Q7s@v3;e`T|TvNnL6E4F0Urv_bVaTZe z6M+Br!wk*=Qs+10$us$-RmdPEB{JwIgm3Z_eB;+A*?KtwNRl8)f*?tdBni;8u`W|0 zBLhhgL>~!j{jHi8nLv_cr0{mQPOn5rAjU*c8LeTh?dRSYR--itLIeask0$_xHGIxh zt7$nF8S0+ww$U>B1WGR*{c}(hgTDG^I-XWanq-oN!}*lkY47uO08rBVagoKipSSF` z94vFG)U0ZbT2?)`eKoCK0H}IT09G$m%AyXIrZq{mDD7aK!)YJ9ze5>E9~ z7Ci?oT*4vTgibAG_0ZykT2LNHjf- z?O~dW<7wZShyC2_hV$Ic{djt*zhQhE4%QDm1xZf=Qh#2|#m>fKXE)hR)i5=CJ9q8J z$E`Bd*N_H4=)H|!0MG`6+q#@5J9f+ZF<7AK;T8PkjKPE-Wd!Z5*|axo20hFG!duXN z3Lr_6fbGfJ1d!?VX*-uKsJ2Na&`*BQZQ2`t`ZI60kN`XrAORBmuhrt9Z6rxbg+J^5 zcJ~fIL`(qxJ0y_$!1j{!CNE?Lx(pdnwQP-TfW2(lAZRB@9; zL{ti%XjT_Zr$>#u4<|7-?+QG z`^-;pcXxMpcXxMpXWVDH+IyzGtG&CrtEH;1-*eCY278}dHL`YPzj_h6ySuyJl*cdZ z^oxIRSm}^`;$Mop{sS%>=i+d})#1Wng)`w!ad)X-h?8%^jZ-+M!$r7L9152|z~xRj zX{GyyMOe7o!nwHHPyPbjnG~ld!1fJKguBDau*h9P_iQd+>FM<1-<0_mI5^}GX$WCA z?y}R`Db7mABAlOwXzCETUA)qr_6cz~DH?iYsZB^^H=S^&tdN`HqHXKgCP{0b=Xu}n z`xQu|Y}>YNm%Gye=nRXF~R`1!r>b2ese+tx;s^uF)+i)BYRDzdsO ztC^X*2e&h8<~gC+&mqi;d1lGXOv#zWK@OuV>Hj_ofB+C|ql;(TZ`-zQ+qP}nm~Gp( zz1237TA2W^*?zyaZQD+3B2!TUbM&;T&ZBme7pzn?Ms5YTySqm2 z`hDMX7cJYiRkzVv`$>+ZCx!n~)c-ayGnZNKY^Z?%Aczhpxw|!Y-+=B=$!x$|{+XEo z&-?!`wU-?(S~YPjzXRSEW>D zMr!EZv3;#||GtBW>h3?`bMV03-5rjfeTGTT2%H=LJ!kgZI5GU;kjT`I`#CeJ@f&CN zbmI%>#^J^rXW=yNb|i4O#91|5&oFRy4R4$^#UXHa*Nv0Nk-}Mt2TtH*CyF=pC#)IL)>4jJ0YudP5;ciC~ciT7&7Y;Yho_YX4TeY2L+r}7k z?S0OVR96-lbfKc!t372q3JQUkr;>Vsb^_fc@RZmBCZQk@ z)PNjYXaUE>CW)_V3PqkYy8$L~h$Xz~(}wM>#+z-9G+-q)@tsO)B-!ewH>-?`rvRdA zzfjQ21LRe&NjbZ=&Nj_R$@xKnTipEK%*QzQLD|_?KidD%{^rl~&tE~@fR>9t287@1 zg|Bvdy!zS+Hws>Hg2tmCBhtmQ;n6e79J3LOMphYCA+iA&hbg$z0{DJaG1@o2sjKX? zP#kI~wf#g7L{nk!0pCqoN4@ORAOR$=-~$YuuVN;F1=U6RF0r$ywj_HC=y;}50tjIV zc1r*&Wss7@hMGmrZ;>>uZaUY)^0`*zpFO;{_?G>PHFw$gpG*T9F2?-d@8i;sob}w@ z!`n|)Tz`5u4mvXswq;<$^;`r#PaQr7bvfORRfW4WHOI3SkW)#5+CTs?NJ3*5 z3w454cyOBYV9vSZ)%R$pX&;XMWS#*P7faq(ezfg%mgOJTNzYu^AgKxk_JX;1hxUyp0Id??edD!yXa5v&6-OEJ=6kW_PAou~#|JW%{URF;$-qdbB zxj*!rjX}LQA?l{iH}MV^z4}WIr~xd{*04ksr=C_ymqxnX#BNpE#xSSS2HKN@j8IA> zwH%j$8| mgs=pz~#~)yF#IVnBG&nU{WQ1Hx^Whdj<(wO-MWa2RvN#Pt|?WzPF`3c zaO&ismg8=uAa4+mFy=R6-m5X(DM!TM5g?XuR_nAF43<_;L_tYdCyhH2z>P&Kz!Lmm zT6JhQzHrS0OKit;ugWpymFxYFAJy!S#~4s~@!$n^e!Jtl;!nQ##C`q7<8ou{^6h>Kp&ueik>1Qsy9UU3M?S(p2@v+Agiyz+FS_0 zb_){llQ{#7Hv{cjAe5HFee#LBT+AAD)HRr3Xx%MUcRO}J-nx#14M=PKo@d85I=yrL z#52#@U%YXH>9(^eBSG7qDCafNZviAKBg#%?jnxPG80w7>$TguU|M8viQ zAYUuMCTl-p_rjOafnR`!?` zH}|&P-b&Oa_gx}h{U(RxcagmAwV_-Aiy=UWZ}Y@K0R$1rscwdfB_gP{)CrXmZn`pm zDK4Zt!eUl-fkhKk8)}a?>F;|kfBb2^z)Q~e}Xu?DyNq0}?s>x?JnEb-;(Ml!~g2tZ=W$>UFQ zpQq_oFFg0+=gw%^o}5?F=a1oz$DG`pSTB3+$VI%`Cj5)}BAZt@th%o|u5h$~ZzHI7 znOXywhp6bHj#fC(W8}8o=?kzGT+qO?2pvQK@;WZ)PR^Rnhd)LCp*!Ywe4|rq_B2bY zE1#sEZa=BthUy*6SIU*wsA&%lOJ0-moT~_$P6(-(b5*?%NYL;a)C*2^h5%^rDrsTP zrFm~C2hW)?BXM`#{Wuzo1Sdk{6-L=fW%T&9yZUMK6X&#G?=eIBXAbl|qrW@676*?E zlF~ddahZFK@n!t>hY$|zu_OUVAVF;`L2VZiF`!1j+Yk|yzw-d(L??W?m$PPWp|_pflCXJ9#`m^Ith77d<#q;mH;FsuL}S( zBEV~8>KYdygggD%iJ^Rf%NelWM#$XC0{dYFEN;Ii!p7wIE|SM}W%p-f9cq;fNIq_V zI_~-mZPKv<tL#e#2w*d zN2LTX=EEYoH13!3;VY#(0=%u2@d{uH0V`L%#VN4$Q^p%-d^Dh?H8LRdk^IXqef#V$ z#@j)^4tN?5L3Lb#9y?^tw_(m-zQ!ZMmWbFT;6Vif34ox|0RjSG@~R&k-!{6|?8&2j zGEk#3f#O*@y3Xd*ufVQGbg#QK8kNq9hwkaI-?7ZR;oxu_S zGP)Os3I#g|Tiyt>_-Z*jMCuBe~$0SmMxaDuW)dguY#_|??ki0unO#gkz^HeAs-MztwhbCC6hpwq3H;~lvRPQo!4)X0Es)1 z6`U5901wI`L{R<1aRmg_B82jsktL>%!zG-?DeDbrIz)gNs5~Z~VmJh7m;Im?;Z#)} z4QOg<&YpWVZL^V_o!bXA@+3J!c5|~DN220YJ^~-6ims8Ui`3N(nj{XHGc3QHhvogF zv0@e_0wBw=h2K&RYIBAR85jsSuGZVbX^WgI=tlqEqKa*?BoY$Za^Sb!ZdVhxfi7sA z8Rs~pWyp{#Wc?+)?=J885Hjad!>0b;t#0|Ky#BkDxs6|>7&o;UTt?Ss060`F;05r9 z2jU^NFv|K~LAKpO)f|%RKJuYT-AEEMxu(1X8KQjvh@f-#6m+Sh4gFZ^x~MrkfRjf@ zax=8_ek&Hh_(jF^$DdRd1CUi=+)x#)5-*rw68YuP0C`y8hhe!lc%sq>M}meY`F;&;Jfz{nUZtS%+jONT#u1X! zb^}SI2?6}m2WBS*jYfBbB3*hQS^r^T=E%yHJOC5A1};My0J;D+WP`?cn;dv}-$4W) zIPgA!!}x-9+f9$iBD5GAx>)s}j$`$CdI{V!J^fg1&)W==fNA zmE<=dbs5B)B>-%3;|iUr0c;VmHN*Ezav}EsA7Kq}hYV)o zbe*+ykUoq69z!&v2t0V{7beV<->7AL02trHFX9Juq#Qm{&cH<)$4dc36HfC04>Hgo z@W6+zkuDA*?Ykh&xhi+u=Kudcw>5e52M`Nvt8rv8@m*?$0STf_gn>|V!sB}ln|`JZ zYbQrm71M^$cKj|`+;sU0)iYNDpAK()d;7}D29tMzP zwbm0N@tBh~>xYCqd>~<(=X@b`fCra9$>DY;NyRc9odhbDPn#e!DkrQ3g`+(%P-kdk!wuY8GUucYf~I#HE1sC+m+gtT)e z2&Mi9swF6^1t?-ja9op@YGb!RK$n;ytH|S&Aob4@H4>2UF?AyTB;pdV+sX|w#Dz^APSx`}b+&ZAa-h%5hJ z*>b#8`S+>kXW1AaAp6F{Qki&96BFct2m>Ml;DkDdw*mwO=&FcN$g_=3h)599@cQA9 z$~i+k1R3DD>lz@M55&$QPV!)o#`qYoTL46aSOn!by+()uLGuR@z$#i-ZMy{`l=`aa z1N{Us;iZ+-|Npj>BP7VwPOm=zHx{>EYi}JEZqS|fSN`Ou`K~r zA0`k!dea>r01sjn;$!a|*gXW*4wOE=M&|ITuafwISh&L<=$hzomq6nF7whKw`87_C zQ+c$9ZhQ)&nQz64v7EEn?D+8SwO-L$y!oxY{mzVydI<)>)0jg+4tWlfhDn1pmIkIZ z@XMgLP%0;o1=(iuoJoc&WlMm%K){5ru^T;nT(>0}r?zF>bWPAxf9fBc%C%$!2gjJU9#semE4mAZ5?Scc0+qf}829E0pnrq5SJ94O{v^)dL zrb+GMHZDWY1g@SYwvprz2wVnow@`l}YgkSaZgXu{_q2!Nx0?i>dJaxTt@i+ydSL1)ParY%fj|hrY6lDh z&3o9^_*nl4?-}rA3-R**_^;pH_4YdFmw*KvH|ST&2Vj`H8;i?y7;qcA7}e^)RHIh1 z)LZY%K<$#e6woCHSZOyHr<5W)3`~eJT#`|J85lQpzSs$F;RoO)Oaar1=a67}6(HuJ zRNH%*qCpb!yWP61cyB)WH@ys~ZyCP3JKc(J>qDnl$dj3?IqnASYR+lhz}*3N4wsk| zpHK%Z^#Bu$`J9H7KZ*G;JudNy?)2<|8dUERUt(INs=VO zcR0Q}B

}Nh>X$oYf|~4baeWTT7^O+o?B_`m~TdfT?$I2Tw-7YRj$NAdymHI|5UW zCAFmm)G3=DvK|mjJ;3A&6AN1tj+Y4sXz*wHgDOuF2=utSl^h6woP?%&=NpzK@AaF0 zwZ8#%Ey6E&tLx*HU|7l$lm#bnpWv8Q(?JuGc;u8;F8Q*K8z?Qdv-a^o^oosSS%tIr@jr!Czm!R*Bad2aUZw`+NGs$7f99vgwW8H(h97sYg$(aTN6H- z5-?Iu9*)^$xOpNuklY(nKn(SX9;ORfJ4iwpj~mCF#&8lbqQC^P!_r6a-NUm6{M917 zUmy5`akseAc9kGBEo{tYj-^KijWyP?!>}JgCUfhKTPCM|>JNDS7CN8-HKTk;?Fro% z4Y00w>A;5CmL6cDZ>W~&aCbKpUs;`%1j#8E)g>75z)8>8$OKpjG&Vwy0;mZ{-EFSw_r1%{{I%nL!It0~yW`E{6LY677zEH1hHeO+d}AGA zO}<(H7wIy|S0qH^j@pIiWf)0p4%4OWS39IZOq~=QSc==!uXhF=s6hkPKQEL!fDEw0 znn@fuBCiu3t*~w^%j+w|rDIF&5_B;>E$#g?Kfx4fW}ko_z}JqHSD~Eb%@VIxZu^O) z5BD4Eiws!N5*V=V=2v27;DQYULRy!kLSl5ke7~;5x;myVOy7xuBQ_6nPej$YwU>=? z=%1HotS2#C05W5AK%4{UAO(PUGj0-fLlZ$n1cKj&;jq9*B2~0Y2hg+&E;J>a^17~0 z`LU@R!6AYp1mX)@q-_(Sye_a?n1CDt%vAO;fRkWLUeN<6=mF`Y&;HMTtO5771RwHO z|LMt1M;o6b*Bvz1EATc5`m1`Wq5&WvO_xHL)7HbHbTX?F%EqR0ut3V)OyXhjlibH=xoopZftBIEy}6^ zNMe8x`Z1ME=_g=|EjAvfip0lK9!{h|&fC4Jeox=_JA(`;X#or)i&u@A+T5X-j1R@;;!&td2yke*`-wKt?I~}7h;K3hEiBN%N%uu0N|hb;nK6%Sc702fAmi7^d+uO^4Ol|FKwPz!fdPhxnzx zzx{!~Up4s*2apzdkvsMIxFXsNs8Q=j2T}n3yZ}!M+sMH!LD^4jNWKZqEjQSnA?V!H z-JNnCu@vB~;@ozkFZCrb1x$g7o?!Hq&DL&E%C>b1DX4c^P{Xaq-5u_&cWJ-she!{@%RjuMZ8sZ}DaqS1$*x)5&pzz+w&Ya;|wDAmM5mz=MS1i6N?H zKcL!$ZR9TVn4g>YwN5?UTXz84HZI`dxO4cL)aW=c*8oiiFu79RMngH!!XF99B@6|c zx>4UCU<1OTaiGC{1ejb8nV{#+I_&OD>wOqsSl1_G3Ab%+FG)y8+@28?*R4WtCn0cT#HDookfn1%*(>hqXKz-Wd`+cSAh z&UdiE$i~Q_$=BqzuE&Z~bTwlMNGUzP0x3o7&`{b&Fy+8Js%`UKtR%>H#|4%f|NHW< z@cS@6|4$Fy?%J+AhJ1OMMgj2TR7w#7@PtGTz(%Oh|IGq;&@NBP;q3w#O8M0Gl%Mb2 zOA}8!2Y}N}()4*>3~J#P~5l+vD!{T|Jw<9 zT_Cj!3(pI;l^_2a9@5XZje|n(AAa~@xu1VLHmRZufDgp4?>Rk2tDKf!YlF!G_DDxV z^(-L)f{)=cfj%v!?J}X!fuGtGEn7lXkTmoJEf~azpMsX|6A-k4;HO8P?LmML&qX|D zw{>Mc^a+`o4pcDX6>Wdoxef}Zr$bo&8sh0|e?jQ|ZT)4s->wqX4Gt4M2=)kYT1ZrPu+TzRnlK2UUjtge zm1${V!L*BH+_D2!glQEePXTnF+7ck)69NQ>JAWY>UAaI8T_B@mg{~v2mW6}I?4mCv z8p`eBLel{x$I=4OReYd)qH7)jhvZC`_MY3vk55~0LFoN8y!ILnOrGdMVCpBoccJ0% zb^#J)qRkWOL`5o_%L zVmBkQhFJHNyAuJvfK%-pl1IwXMXM=<4M5U1Qu`g8hkL+Dd^v-D2*RT`T0C|Rx!Fk! z`kQQjMm}+<5LM;mU(CiE^+MFT?atz&jun>#w(J!pHDYFT;o+OyUj6NMU5SK!t8Q2Y)RwPfJ9B7x)K;=el z&O=FdBeyYnk`m{JTwM_mz;T=}{tIjQ(W*@EXkg42zHk{6F))Jcb%NG7*R@x$dj*9% zk|6?6ck`$|4|xl@f^kLIuE+A2Oj9yWlXgR|f#&Hf}J&cAMlt@~Q=3iH$mm3JAdI zKai!O_V#CPKG_+I)#SQ`tlWBFF_=e> zV=LH?CYq)35fx%C;)&?^0^8*hPq+CJicpzKsp)q@&MD>K)}HR>*$;W(0P=MF=%QLc-BC0C8F6690fKTwNcDcou&}z!txV0C)*U65McY)k{&;4if+4%83J`{%PeJ|CRuy4` zaZAp@<5uVCu!}im4-Q@lqCDh;CZshd_+3?$3u&Sub@t}TH&=9bGh()EWdbB_ z6WKpOiO(M^>NUQ|Gs^ril1F763hR@7?>+l3d zDg(Evlb!FXl<6`Qe7|PGsd5U49c&{4mN26nkL_n!`lpN?MF6H6EP(gm83!PMmrl?D zoDiQJU~*v~-7b%S7imi&q<|x)!Vt&dH~|OOylG9(fH_ zcE_%Jgr->(Q_dqHKp+6eScUo!K)g`EKmpQFRc-^?;P!GNgb)W#fx9B)2&0K`h^n$r z0>NDl7YS~k2_aZdJw&NnfoKDRRj6)*>h2oYLMsP>XyBm)=KQm~XWCn;-T~3uO^KG% z0N~Z@!sT$57ehRqJfXv+o~~iiuwIwpmo*Sd5H`WXvswqf-;y1;6APC(XUlGBS-oNV zjBJxY<#F5P2fpc-HXf<34qG)7a22?4pmv`T?de@aQ@K6InC!-oY28^m8pfRvaVVNnT;-s>^=6mH<6cp3 zxEu%XQQJ^SmJFVpuzv3|Kk|=H=#MT{+gSQOgXM?2>DSDQ%bfD^VJ&|Rjbpkr_0PJx z9pOY*H{l^negIFmX`@86P=IaJmC7zTB=-m*fS__EX5unqT)nXY5d(im5b`{PTwE$q-xC}54N8=Y`)kOgUGOJ&!lNoXK|FV>PTRCyAS- z{!UN%;4R+q(>}&u{36~@D&g?A=|!1tJ9?TxuRN@}W6*`}z=Lm2z3c%HQ%QnySNik; zJ^?~d`9$^YI?4?m5{&tPu>XWKJgxTB#Oe_d9mI3c!HI4H0_ai*T{KibK@+4mh;lFm zXeJrx8V+!BY7-6(wDCj(EqE2hw9=;Rygc;crF6%^{EKxZ8E`y}VKA7p_I+>S8!xo_ z*(a@c6XAv2yI+~h=hkmxv>dn9UYnYJDsG=y^$Bl6%gnfwLxn)0MyOKDQ}M07_Wp^F zy!etY_-g~JXTIKZ^?7q+_jrgWm)!_V##*W>6QKfI0JPi1v%5kdKmjrcG3JWu52Q5! z(oj);N=||p$)&CV*%G1_00HDNmjns91W1CEN5H}S#F$54#ZQPzDyWQ0!huM*0Rbek zu<|rTIb~wXk@7ewYgD4EH$k}-Xn1NUtNh7d`K!9}qL>>eZO?^Y2~vP|08f|O#b;Q1 z={d%qy`E2~414kv0?yvC!zlYyY+C#49E}T}Isow|+rF@u_7pYb7El^dx<=fdjIZ~$ zAJFFHrC;zjk3RBo#yr|M8ztdX=IR&3@C4G6$`0os%6?BtdC~pAcucfAbSESzg0CE& z83BZVA#vdN1~nD+CAx;Ms%;z8w%*`ObVNb0rB2ccbkN`e!T4S=9iY`!TH4(pf^udg zkD0;22+FY%lv`~ZD%*f#pV+3h^ir0OTjrjvVOot;NIO%Y0>!2CkEln_w*HxC-2Bv> zFbZ$%nGEg7Zb02nh69@HA!=tT;EqRJo+DF8Cni6{6tRE(KS2JnWbSW6>}0BhF< zsfyEX$wTP-m(65bE(K{Gx&jeItXXB|ah}uDOg`~U<4@mYdV{N$pcmmi+B_~h-Ey6vy@uHBT0 z3nCRlI~#3SN}bhOwof8HJ@Xl!eecfGm;qU{`XXdtB#*hGTJ$uK21o<&q2Z~4rv@1F zn7Q?wVa#7GYBS}-6r`fEgC==h0^CqoOPP`C4q^zTO+YSH3sz8>U5lIJvw!C+Cm*xv zmaR6QEp=<ei1U|0t}Qz*NWRpJf&zanff^c zT~iWb1~OkMQm&~E%3}sbGPjhNflMZ4NJ9Y96exgUq`Er+WKxV|4wf{KZjLVgtuGw? z^Y7XD54?F`>4h~6c<4UaDEAD*11~sx)02gix9zGO^2)^K^5? zbePn;tfqQ2!n1Cj&V6bBolH5Tn{6gmQa$4cY=8ik)(3sfG8l4m(vr39JbQE>^PUsk z!`9h%tZ%pNae*Al`AR8p;XDfW2{=u17F3Jb*o~14G8psGVuTE19>K?z40N1;@qnS? zz!Anwc_6xUp?c1gbH$jy0@RLVM)H_P$kdPIkqpdaAgSazb6cN4DUTV5Qf)qDr2S@h zy!y91cl6J?8~>sA&-VM#=kiib-DquvnyFT@XB;oR$i~M#&+Ma4LkxrAvm>v#!{QLI zCsWp$xXiTcAJF8wbIJ`-yF{JaidgGXL%P}hjTi=ovbCWaWmmv^aIut+s<}cyG)XcS zu&_iwrMftw3D72f<>6*68;{nSm|I;1N= zW-J}-iE2||%+xxNhhZnd$Q_Qk;=rQHfHcPpWFX~=1bL*^nHYh#YRp)sNPZu*`xaMA z#mUjX`?b|S{Vx3b-dNq7oV-EglEKvnr2B@CLF1iY`;>Qn>yysP!hRp58(-RB3R9aQ zca?F-#4_D1DaxL*1qUNjP2FN+gW`~Cwnl7!)nzC^VGk}O4kzR7Ozbz7MhJO?)%?f= zCr!0WWY^SpiJG|^>K8{jzrB}lJp5o8%Og2cVJN560D;9MS(^tIbD!eQGMwevH#m@M z_=^0~iP$Yn`Un|{KoHZAJ`jkn5XAyR8b||)0Z!-uFlHF@NQ1L>l;;fNc4N)q!^we; zkdPqsgMw=OPR4v#^-KxK338%-%q4I#0YoW-p#vAo(fV#3{D(hy<)3*P|K?{7ZjM$q zZeS$I;Gi6r;W=jyyepfZ^USkLGch}|l#H%ftE^@6|NDRMSM^;@sEbWg4-W6pk%*0MKaQ;Vb>PgO8Y$a85cmVcwc2GjO|wrwL9uTW;md^5k$io67Wd z!-uamdgGl2j}2PQh7upC2c!qi7i}gyrjj!>x)xK&1YJXs-p2=M=%5HWoq`YSLU-D2 z8p*pLIsj=6K5+Xnla{vOX*cMkbis%VFgT@%PY-JTyI=cLa=z4a=3t4^YRwR47!8tS zhtZCZr>42n7MAv?3{nP4A<_&#t_rcaL36|I$@Ep;_ldW>>r=*bE}f(!a2i^HIBd?$dCpZ5hs|Ve zd9;gX!CM7@#@SkJ;hChnt}dV0$4Na-PA;qJNGwlux%MLrNYYrsFEe4sju&yt z;}DNXw5v=V4cIvZD@{*rPcV`~n>3?vZo%9JdkuV_fgdo4{RY0EjYN!01Ic$hbb3dq zOc24ztsW{F%eZa{cyM?LQ5pm?-HGEL9yGhqY(fDx{Cn7q0_6A!kQ!j1kjteb zIJiMjLx&VDNTfvu<_yf4+c(Dn0Kyu=9VWC(GNVpUK)Tl$_wI01AImM-w+xx5c^ryN z1z46EX>5s^;Aj&FByou~Z}#wfFTeA=UwLEB7HY>{W5;lbPTckRajzc@aQ)XlzWA%} z7hh9WQ@{oaP?F`W94>$>t86V@H~syxHRi3l)e<*2X3U9OoXVM6t+56+(;Iwh{P-WL z$3NY2C>)M;agzgsG=Z21aGIwyl#-0KdxVBrZ8>a&8xC-zp&!wXtV?y=Ei~~G)+VOZoz@3 z>lzMD-fW0P2a#g#gss?t_uK*PVN0ezx(9HF#9iP^vi&pWa;lcs1r7mQcfhhhBplT8 zoKiZsos70Y|GY2I3p}55w;;?jG3R8tMH0iV-{-x2*vAVacDrWs=3lhG(i-RlLj~1s zVL5jx-cc~P?#YK4?h> zKU;CU zG@mjC^Q0-y+WA7ep8r!3+U;( z#-{)hK(-QcNk|9aU3k^e2*mjSgft;U@)RAC96WUmAE|sF9U!dfvPtfa>t?TJ-ldvR zXGK`rQmExC`OqwFRazu8Ny=3Ony!awK{SOw?REG}KG*i!HgJp&R-wP1R1949=A&Lb z%EXq1D@O6kX54JWb+W|@mY~D#>Fh6bHMJel0s^Q2$|mE`=SL}DZgsLh-ljZl*WLEG z$$7F%=P^HP^c->?KEOvlB(G9Hk$|*SA%p}-Q(7K}96@aG(dEIxApi-9 zQbTk?^B_V1(m|yESJOmANGAxkhKiV0^v5q}+`emNH`@vT7Pi>{N};97CWkg@t)QeS zA*Gx%Z?){7)fY53JXo*fasnKXisb4?+Qa*)azQaTMQ@8RTk>WLZkrnA2{#hp*lo(9 z4NV(>@bvyX@7EJX_e<6w%hclQ?R=>%?g$)lG#j<2IqU&XrH}BA@^+swSygG^k%Ht@ z*$!b7DCOGBnGGk(tp*^17`)lvdf30neC#`Od8SeT&Ud5C1J;420$mf_GEupaHmVaL zdH>Jz}w&%xkD(w#jER@cc$UtDVYW(g}F{e2Wf<*loeH@+c1V=7%Yrmjktj zxmxeXa^6<&&a5xB;sxPZc1xlB*L(ZV#8FQnS=3sPl#<0BX>XQ5n&W`I+hdo@iX4__ ze_Gw2mb;Z6AH|Sj0jn#Z6iSAk>3R_EI zNuzYu?9dH)_9wWJZycf1eK2nZWby4GxU~x-9n>}4URYH-lDp1)hiMGSXq>JXlskiGo9K>O^SO3^ zq{X)$OR%kADR?aOnHt(IUQHp9_iK!jKGl&~@N zPrSJLj26e1!750%CP@xd0LuAl>2$AXGzWoXcoDxekvKb<&Kp*1Xy3+escVwA>AI;uH~{s~Vx{GeA3$vNeY|6V>)O zY;KQ7Z^egq;ggJwC?FE1hqY(jaQ^D0w$zi(+5(}mf@R0blxMp0Gds%noPGX4C+ns=Hm2#r>-U@_Mj;xmmu@f;gJ;d=eGKkMYEy0o@F zG!;T^ZXMj{aax0+WVlK3ye~c+C?BS^W^ribIF&GKIhA{8QI*4MiQs9o_$lEor&nW2 z<>WAV%_G)Oy<9CIEa;Gi&i>6GN3pDj0>{yM-6$E`*KQ=YP4%R;-fHo4Exw(^;!|;} zGG2}@k8(DV@WL@hX-?UiAuaO$)%f~r;_VftaNQjM`6BW^UQ-_SMJj+c)WFglt*x zU^Ro!J^6_*y!n|07DsR=t!$t&xgNmTEP=BbDnp;qI)!CQ7b!i<#D?kc@4vPPG&HBd zEoMTg06i z^Djhr(d^S^pSASYl0MakaeFrn0$&-OBU zpT7B~naM%BM1%{OXYQ~78<5zN?L<#G0X<2`xkNe7zTU+JiJ>$rmMh9L3{i3Xzy2#8 z)W=Eteb|*@4bN&47ogd@=7aP2hW*W3PM$lWo+Vk&$6)R3D@OguK1-IX{`O(t=)u77 z!%XZ2D~BSj6amU@T-^<1D(08WZEFB1RaB=dQPT{4OyrEyvT-8W07@FBL5)gJy`{dT zxeX#&05JO4x1`(eNgqHGl+sb2NT`*yvfxa`dCcIjXiYexYhy_CvzFn>D zKIXgWA?IoB>D-^|SX}vbNzW#%vWkQ;AuU4MR{s|Hjn4$ zpMozw37>CQTye#;c_rhvZ*;$B_RYSTM@*ZBt-zVI2cnd1NGplugnckAX?{1If>|G0 zn~MfVL!OfvcroF*hB8rhCI-H60YDcF?t1DyBX&DIS1YBVv<>S-dnz~wm`fxxL!bbPT}!tu0I-pO4M@OtfN041Iy0}zFxLbOz)U&MKn7wKAQ#jC zH~*ddqi*}ozxU-mM)NFK7CdlBH!ALE06{>$zfK9&=haJ}4S%6vsI8=R_sr{EbFUkN zOBUm`Zes*$qRJ_Fmvvc7fxd#K>IYLTNkvR+IAXM})Q|>2!z!-09jC|?j_;6TMVFa-Ju800_t0sc9sfA`=1b7wiY0yUl8pNWvH z+-Q{dl(rhyA?iMme(5dZ*7^2-r+fB|&bgP3;U$w_vEH&65W1`)tkyZyOD+d`Jzp{5 z?78iJ#vG85n=g*-I#fZ~oH8RNnI7Hh>n*&oYcGyQ0ql!du0XVjkbArQ+vvkN40qg((2Ojs)gAe*IKFM@jg40Yq2tWqp;v_C)Dczw2m5ncl9mG8c z>X0@dAp~Aggbf&BJD?e${{~?Cde_dr_=Gq9fmgWU4}YW^|Hx-O_m!ug{oelVIYX(~ za5aZGZ0gCt>s@+Vaz`%AZeZDm_l#zF#afd9a4I(OGgaf`DYC=tW9q)p9Q9Bm>U02R zjYj_+hMIF+CCLAnNS~6P@E}>$3v~*5#>1=s?pHUj%1NISgHX9#00!sBnQ-anvbroYeL#7-`0)>6ckkW z*HdehTEl7eK3fLtpQhuF>7)}iQ-e@@Gco|DkhP$pQHw}mtcErQH$*hT>#{zF(Ta#P zx3pRfvgdwnIDXr`9BZf>@{Xd771MUf5owj@@KVeG5+F>I@dj9Os-w`8C`nWt7#l)z z4wOI|2MFN+Abd}|6}*2v$<&E}1j&eoI`bCx>tWqmWh)bO5Vooums&WnypDywmgCQ}rE;K9>&NnzHMAfQ}f%sil zsg9=7`~LruWTy%mC}^!=mlb!oTA*f4eFmqE>ds__vEnQipGX}~yX+Ft(o89cL)=4+J{yv@1J=-EKj-#hR_t>4wqi8$kxq3Y`XMrS7KhVvoM|_v z$SLVrlW!(@D%e>P+cckM`0N+K>9x^uNz0WiiBWONVZf@Z?3aF9m+&e89qDpCCzi~b z1o2Ka?y+o}e46BO5>sza+S@d)4DHW4;~Wn;3*XBDpp*W2KHwM!*4o}leuRit%F+&) z&)Dr5KfeW+* z=s-$)N&==H!(Ye1zA_Yy;J~8Fb;;8vX&aqeh7btRqLfoWp|DVXOwP_h=m~JVN&fgr zrXXqSmmtvAh{Ear9RP(wK%`_0pls$;0-0>Q+PxnbGSem_F3V98Qi%voosr|PGni9b z|H4fEgcjZvm`*s@e5J&?*Y9-m^-Oup=)>!@x1}mMo>c?1k9TqMTS&}wp2^5FVJP}LCoh)Zr-H=!c zg!TxRJ)%B}#497bnuNd*eUAuSz5)b9MZjKh*HQ@QxFWlOgmBx&xz&mx<&?>Zw*wHV zUc}?|T>=q~p4B&clE=kch=K;x5SU@0Ry;wrw3aB-Q%+2loNRN;jo*VkPA&~jdAnM= zv$^6)V{alxULk=0E~!`DHvrIlS-OE>GBugV z>&YB`c2@b+FV-g`hPlN-?yNcfbW=Us%@bxkF2zAftVm3Bo#~E$xJ#-P!O)KW?(QHI z05zI-JjT9n`1Guy#ut4H)DD)KYhr^XG7cR*e6lB=bbT2hQL2g+SipggFt8pND4T{@ znznZcEGB`SoUoQ4Y=9)>0I9eP)F&L0A5fV*In9SStRdwGAL#l3&L07w{CV#0G}pna zg;A)GA<#Jq5oN*mB|-uP#Y$}*Fu@Sh#l7#LKqsSFY#X=6*L&04x!E_HeVcps za2@MS1*jsYK{1aRK3Rq@NFyl{TJjppEW;!z4TBskFRL<`lh%FiZ_}%jOPU5uG$7Ej0N;df zb334wG3fi-bCGBawrIQ#qsOp07<$6w^5@y|LtD1$(64kDN7ItVq9UU{=O|6XqS#H^ zh-*h`qc2wN4O<6OY@J?Sr^zJ`zw!kRxx*035+NKOzb8vuY*T>Y*2@F6jwm36@!>(@B6>P#+Bsw=nfwQrI_o2ZR9c5vV{%* zjq;)NE(As5knefI*l&1VH(3KC%4W^Cj5-5IU~7*u8QOytz|d~W^hvf>`~i0*{DlkU zipx+6xy`{Z3f9-8LMcZy+eJ)7gdT93Vt<=py^7m})jwQqsAH;89S6)oCn2jzgu!v{ z*5FOacCNH{xs$?VrCFBZOt~$jEJIRd?cduPE~(*$g0AOObsWG>jyz}&s_|j9B?k?} z?-q@ncev!OWW=brY`UQN#h=O=)B?~&l~Wo@k$o^cx5%l7b9i{|Kcxj=eus;5hQHRoV%G2>^BgH~076W1#04qH02(fDf1*315!e zw|+aQrnSG7{SKzG?=>rVPzr#8e%ROTuPR_Wb}WP~raeX}5ZTA(?Ee*OB433eSOauA zsu(>)H5-S%=m1#lhez^QhSAkj}$JB$JSgakP74GK&ji<6@PdKQ)8;U(}{ zz<)x$KWQ}D-HxlYE~;^uTUfG1iCY3>|u6HMbtvT5|LvxBloHfn8 zl)vZNKk-kHvl!S6;5?0I29}0)grXTN!7hPW6Hl6XE2$eSJBP#n-f3QB;u6s{0_Ai8 zEbr|1wQDuM{_SnRolrjP{;N=j6oxC#aODI<$lYWoe=S|LtGj>i`|-npAq{QV&d7`DE2VdsV&&wLr&l zgi5m-0dB5}!EnT76UK7EFXS!X>FM>^B|W8CV#L_OnbTNu4|&GXXjZ-P=F<4aU-#d} zSV_eJU=?=eSH~^?Q0a)lmKy22HZRUrb6WwoQH>MJ8_r$N z*50AEscW#gJ!d?yoG^^(yy(pbeSVzb71L;y0881q-9O^R`s#9it4BXJc=5{|oJwKi z=5mZ$n(T38tjuKo=5GhphA1XK=x}P9AFX1-(IV64G!$&)RVmeM63&b*PaQzrBkCB6 z74ucEF7{sEu0PY6eV;-M&5)1vQ1zUc^-I_ z>s{T6QO^U_j+2v5_CY>&?6l;7k}4E4Wi!Af_n6|7PJU-a`EKvvy-fQoSLaYrsHuh? z3b~Qf<0C8Ln>~D!p_jhwDesnDo%ffdFR%IVcq#k9)KKeydr0&~^AF7p3-?@|1>uaK zV@jX6HNh{b3$&$`<;R>UhS^xd={CgBD*My!ex zjRESU9vLP7ZXZdDAD`u`Eb0w!FwR4v&#Kj#QyYQAAq90e)_Iv8S`$#FSu=t{dNi+ejELE0uw&^G zF@C_Z?|0D6;$z<8_Fdok88LnrC-uHz8Z1h$-s+8aSCk+2G>@t2tDMHD0udv=s;Scf zaFEDw{J+EFU^Gv=_4XfRgz2B zKRpU0M#k33gT{ZcgLiGp$9Bg1-h*4YoN|@YyHF*ws+HRFzT0Ip_Q!{v@pdoob@6ps z3K6v*>j^y#n3Y+Zzv&x7WZr-|JICu@?Yy8+m=W2@+CeT1^9{{yM%#-dvu)qUd?&f9 zb-qUBWBg_3WB%2de=(5QG%>LSSWxUjp3|B(kbuwqJOMp;5eO&R84-D-r3n<2QQUj`gxSnU?mnkne3Dbu>RtwU@s=qMcJ3wNW;6oGLby z0yHBo7@p#=k*RdKS`n}I4BpQ%CPxZK*P)xSpYP7YDIU0Dz`i;GoS@$0&VS6#26tF- z`IN)k>N_5ge{ZMm_|A~t4T11Oo4^qr=|Xw1Vf!1oQ^H^>|EJ~cYdwSeL@0Y%MVBE> z>iHJ8v+vwds5jj7*3>e8Xn4YyDKiNuaw?yTC@gpFXgb!(OtQob`aR%ZXAa|#0E3_) zJ0JOXTOa!$ESc6RUF_N2fFU2j3BZ(cfqJSXzy_P#9CHb~cg{*>QDvu~V1PIpfdFyX zz=d3_!fH)V1q2Ww1L*+0@tTYX#Z8y0tv9`}`WKOsL1Upvd9Kx=}JY3Bv6<8S@WnEH_ zu&cYaNss;Gf5!_wS$@x2gdhauvO{H}n*dO&5$V>vpG5640Sh!NI1Lv^fjna;GwWZ> zFvF}LcED^}I@remGLkgI3}oaoaLLGOLO@yL3An}5eE<%pp87d>@t4m^g!M|kzEy~hj+tFNnrM!r)Q}?p$LukbpXO+wmX(3pmQdRu0YHRA4Dev7 zZe!$g;h>|pR5+s%G>Sb+rvU?@$jvEe6*#97eUjGEt_@*`u!g~2A{CGf){Hm0ikuEp z-~E2BI5X3AEa#XBA);v{^=|8zN@n})1Nz55^v*paOePvvTUjm_5ghhVQ~5qtO|qYp z>>*l$boM4df#~6!9-H3$5z8+L+U%*;#FgaoD>nh_v`1GyDdxfGfN z`2lks44>k;`VepUk77_pi2JU1&nzI0;UIFc97s2Lc=}7d{YQOZePOzgWxV0lAi7)q z!lynLH?%NRQ2O7z%sc@hOY)$Q8=8V^P81fdals}X*RoZ)p|Y9a4}S||l0up_3w8)K zH4fDVJrkZQerGdccEZg8YYXRIy#iQF$S^M#D6E=Ghb@nI4@sOmGLh3j5Lopdic=)q z;0lPgFKU$;&)@zNPG~_yMp%Xco15|RUb{J-`%wbqm-_(TpS*L=bjKth8HVTv4H6Rs zlVdyp2=EC+s)edXfI+0kfu_{T317{MWTtM5VRwIlp?50hQU8AlvzDz zd)a4i43*2A>CX2?|G6mx3aXQJAi$|++UeB0y-hqL`)2yl`}h9BJNNXF%;FQ_*a**W zVIgB@7y!WU^^*7!=iO)|Q7Z=OsNJ`&5fXp1UnM?J_<+2Ay*h=~a`vDiozlvOV0Xq=qe#%5T6q;{u@mL%RsoqoDa z(rM7L5R*AtS=k~?9Vn=PAXHO|0RqsvT8$lNCCEVx9d;O0#%RrYmUgFmNc~jjqAu3H zf`y=7cQ)%KqCGQ{i9}=oNkhy-!<<&WG@(j}Mq}Kka&Z6^@zf!gS%n1m;hPHl`qFb5 z*!rs^55bsG4L1%sCz`aeZdHc|QUl0}PG`^w$uyz!L{2GlM<^fx3UeT-+Ga#ArO^(iG)&8?^&201MsZG6oBR5kg@w4ZE6W zY}Hu~g@t%1MzEZ**wRW#q61t(5NtOy8eYN%m%;QFDoQ{mf^#MK!|pJw>^bXBvzCc# zrV2$6VxvB1tG*mc>&7*lU{tSY2#sL^T%w9@_*I=YC3(P})I598W?&E}owOxq8csp) zT<$^IMV>dY5jK~%eUtduJR^maFcTCP@{Vm!B@kt8#*Ru=H7=bO&D{F;Mp0#nE>x-1 z6J|XLz`hMzz=zz8An9s$kAX8&2*(^!h*O?(6%(EilA}f&Kfz%#m1$axN}v%=(kaTJ zvgZ$%(&IgVt||cjf3u6R&i9GB=Yxe!J4@BoKKrbBe#{nC3t7+#fJ%ikwmpTEZ z&OQY=HQ5{L37zQpeHZnVR!SP$-SP;^iOE#BO3W!iaj6U{kOEmEswPHd z|Gk3k=BbmlSoV6ls3hv@P}n(v>B5A2Lv#u<}x?VKiOqFqEo4K7X7JYj$aL`a^8-1mX}a2&*&2mzdR?1B3gpcAT-b&bc7$+k$C zl>KRvW)w)mA2iobFtohSSLv)~tY0OjUfEC@C|(_Q*o{c%ZuD8--t(xDlhzKW9dvT+hEoV$9Q9dAAEp4XdZmKP40 zdT5ZZFCSkeofaC*UE5qL3bU+N1 zt#M0sN5?+$iF@4plTJBxCsl3U;`#6{mu7#n7nTo1@lXLU;`6mM1Y9M?_tYm3$GYuE-r55Xa(C(H7ZcfvrSjZxZ*-E zD<8QEYhd4Hqs7q(MpVj9UxPzPAs&_p+6clv;N###0E7cCXg`HU@UuN@fYF*;!$t_}GzXT~wSH$_9*~JciV9`s1(l zreAaahkhcPPP5Z{L>%Vressd)A7{ua8X;Tozv4s)6v7K@dH9w6_mfl_CK$EoWc03f zmgVXP1D|{sa-cY1!)=Xs)MGXt7e{tI`b$k4;ci2`SQ2I_PRPS4@QB3uH;9si3oENa z8xbD~+kmg_vW1@0u@WHx5gtr-Ts#_6sG1De4R&k9S(hIS81;7&h^Q2yB14jH(4~VB zSBWZ~DI41cmZ@^lXaFXFO%omFFEk(7-f@rB^mwY^YcZ$?>~%)-vQC16){o6zwluwB z6_lw@`pl<4(Y>W_?UuW9%qlP`ZW=v%o@@TU8y$RwI1XE05fer2!2-%?N5>%rx+Y5G zDy_h+Hg+(TQpMek!6C140m>FfUi9!!e}#YE^_;s)32B$4;?h~s9q;;yr=GaWWqQK3 z>H|=cmJ6piQLWe$5%${!-i#6i3hBx0*eB{$+-Njxz=JbXAPx{WSJvizPi&Z+&SBf> zUn>J{FvxJcu#g}U;nz7Y69b@0k`+B4C7@c@d}xW#v-J$^z6c|?Ht!UnUjU*lf-T?% z(S7VDd-rj-;L%VXz6VTS2sEH=`9@F)Az$UDca=TX56`fYYsI~aJ98Mqew;HF4v|<3 z6=0&avc`lS|2H$+v5$#WX)|A(v!oe|HJUJE2jEnkTM6|S=1`O{f_1Gt_j3Q1x>jE_ z_4iV`&JRCGcD!RMZXP}T^l#Swdwxi$mEA?6krFrv&jYd?F%#3Qi@OXah?6GC^e9^}fM?r?{o7@nD)hkfQ{~ zHbLc?D8iro374*O?(FyU-XIg7jC=ybeh)}MA>M={m@WINKYsLyfINDFRl^R_ZecfA zsPvV4>u8QQ!hNlv6WEO|#%|%Ge_yEgtWW>NFHcO*6QH+Az&6Q5+AhPHi<;J+;e#U9 zem>j-6Pl4grPgPrC{DS7{o6Y33D<$zNbrTbf;How^=1N$fxN1%AYvW5f$Y22dh<_T zdFD2QF@#lko|*L0qeUC}E&J@-kH6_On0&#~d!N~@V{-nS2xacZszHG2uGyUHsqF(oQl1J3f~y-CPgP&nRsHp&;$JhZ*$fE|CCpLPDWA18nk7hP*vHayP244t+{;_ z>W`hM9o&g9R3<=xtBT$`z5iFf{_B1>%cd#fO&`5~&=O4@yueL&KiQ*MDRafSMY4dE z-Z@_Y=S?&Lo*F5Uq}`q8p0!{n6el_43+uFJV02xyN%h9(rxOthWKu;tbrm(dlnAsRTB7+P*z`^djH?L74f>^1N2c;Z1d^3;cWcYN#*T187zIxStz z3wNxC>^QzKgJ>l}SFLfg9%_4Siwo=9J$GQ7JI*74V6ZP;A^8p!AKrU@ho5m6b>y6!}7-#s6Gthd?$`yOy8E@UfL_Th1w z=QAYNZr8hBPtYdrpRs~vVNgpscQbsi7KqyA8(w$njDe(<5V$qpPqFlJEVv5M(v|5^ zM^(?dp0IArmtLd!mt3;lj@&*`|10}kL(=$M*A6#1xGY)YzKf};*tM6GCyVu1sdtTN z6To2fX|xoEszPL>atAMWk2=fcYptZ8v&`#LbGbiHMFRSGT)1Nv%#)AjlaBMCB3JE2tzeeu*1$!dWmIjANs!3%tvxt=IY>CW)=^=aeO|x{AE+HoY%k_KwA^27;Sc#rOE+7bDyT^UOrgPJ^v;al&vn{{x z(~sZorCdJS`X^pDzsh0Thi`mv^E}sXnCb>g5op*3Dq~L7nRE=_OKVTWyJ}(G!sy^Q zsYe7O<0P3lD*SMo((ZeXEVGGE^+EjaK1%$PRy693^Sf`BKfthd9NxEgzqdZ)%Uz(t z$F5FbAA=B)Bqd;4BuvklO}tbA>ab317H_zz3NF=68AvFg0;6nO`GC>Lj z;1Gv#+TPpoao+VG4&E;U)z~EjNLj?`Yt8!fG*Oibk%R6)33(s@iI5TyA-kzW_#))6 z5tOpQ##W@gF>be5l_&BjZx>XxUT*k7z|!Y$d-O5y&>@$+FNp>`t<3>EN|lER8S}a| zSMGIV$Pvdj(T*a36^!bH^S7ukKGBU`II#=e(tGK!02x@xgB!To;r;yX57;B&BhO7v zJN+=-Ih4=c%kP}xB_Z^7Yu>{0v$-w!U^8K=AN0+egtfs9JkYe)(9^02FoxYy%1^D2l?Q?221|3WQRBJWSz3 z&I-?CkZvs85g+^A|KZ@helF4L)&>4x0`Z{GwKwGy+m57u2FcY&RBewckL-Ut6Kpz<#w2LPl&r44> zxft-^K?6_(5ULh4S1fI-D#}6SUKIf!0tq zM>GNbs)F1+J(*!zmh;YjVknD@t1^AJVhU(qji2h>8`mFbwaGH$H3<-1XS)-g;9*zn zn5%*WzV$uC3;_M+)wL=&R8rnHK+zB2^-j_cUAcnnzW_jJDz=Wc`vWSOVlj5&o%dZE zn*{}HpWXPF@A?n>?x^)3Z^|N(?k5>sz_pu{Yns|kh+yhM2y*6e^_CxPXTjSHhJ@T` zY8-07TlE5PxDS8a`ZrTD4y(i39Gf|R^)c^=>tFU`sYZV0cb3LzU|HQRnzW?6ES5LMPw}Wr$Dq4!_^Ync&^3z<{ds9zh2Hj?8Acx4FwQ= zg0;&Rd(nR8;_9?9S0+j7>McXs(+P>qR@%&%)7pOUGBzrjB0Y8Ao()&Qm zo5*S&wXGpgh3*-re#REvcZF^y)Cy14;F%2Jb-p=v z$rX{OJpl+xfV1W=KgJ!p(KWAIdbm+xf^s-Zm+y1dMIp;$w6_^{33M8z zi_s3;&Os7<#)(xP-!wxNyoeWmsc!{B2cY#e6hmlwxUa`jKKMAZ73*Z4oo;e zXwwT;uq(p9fdGQY_PF*%CurHzSNc7HYNAjbUP~yAgAej!z31OcLS%5tJC8&8s}gdk z&1y3NrHFvQ7YY%&fsH^=tBAD=xv&+p9f4^R5f0Ea+?L2n*&s780}*&(7z-MA`Y5;O zn_T;S#lSO3g;*OZ8w}v0Kh8^xiE}c?y&v~LU&$3c_F%+2l05o%{=!`YWw=z1uOTl2 z2VXL=3*(j^kLiG|9XGwPc&*#3F+O4x9=Mwa2cej(X>R!2R__&5JGKH6+HvzciAv9po89GkqVQni+r4gexkJyW0|aK^q#mX(_wnkd42 zckrzeZaHKPl#2metFy?(@{2t=BIJvt9HOaP@M;93C_V#=R)DzoOi8H^A$ zrhElV4vixL(K&#-SJ*B!M>1B(Mbv&kXPzq=l0XoEheX;fuKNM-@U))x_D{-6Db8Uf z-v8Hy#3a~m=ho{EbIAh{jMlfvjZ^T`oe?^_Ob}6QtYPGxNqD$QM|9w@9DPllKWt3q zul+`y`0&gQ;Xe?o1bE=q!oADeyn?m@#SYZ>irZkF}nDyZ*fjkC^v6JsI)aumN z_G{ahk4aa9I?FS0XXsGF_kAL8K^S@!n`;|@*|QLYRe%{(At$jetiDo&!D3JFd$a)& zjTc~1fQS%r>&JTO>4EPfjzR%)m{~W|c)G;j6ES90YA@6qsl_y z{*C!U{m6183G5&bCxq}$9&-G2$8hC<#a!p-NwDFAQ?&7JRT zUg^L8((S&aY2Vs2IeNlQ7Fej}0(v-EP>oHf_p0x9UE*_p?!Lo z!Ba)3!@=Kav0s(ARMNqVs^c7TTd>ZBQ-f-PA0o2JD>&op-UZt>wkZq7D=Lm&XF`WE2(Ho3dc zr@D{yXMgqypKG_gHD)i?aUz_&Qm~Og1U`IMf+Hyf$zCTFAG68eo5-hOD5SaG?rt1p zvHepPD8wj^Xy#;~eYTk2lv>3crj;}(gUoRW_~X+IdcPzX>Iy@tlmLROmEdexC=R*p ztk||EIO^6A#|TUUM6l9OfV`Ouw1o?3$O0xZtUquAQAU+BGF&dj_hfsO$R^1y&l0TS zKi{i+R|2CM@9Hry7%>6^LE9SU6}AK<`v@SC&Q3wQJ%I%Dn8&GFO%iTft9^H*{dXB_ zcZ0q^?&?@8AFkr24_z(oe_;8{x2;;SU(j_zNv-VKa>5g{X}jd39J!&8Jr383`l9Qs=cB2>jdEGmLZGXOJa zV+}d$lC%a87IP;BaN}qJRqRI&FGv!9R-qI_4dcAe&nSgd7%S1!e74jt8_koe5|}f# z)3t|p;eBeht~;SpT$(cL7@1BWAWzF}k^?jfyX5GkM9z&zkEkdCK#AG?jc;AP#P=RP z`=8L$zj|}nRjb6@6~#fi5&!@`EcXI0%qK^iXS;Pca%nrW2}*S(zAe|DO)qw*+%Az>Q1f(}h+XehvO{@t3M zK)Ng7KLo$#voZ+zTn2bG=qn}5{wHrEU}%z_l~V1~#eCUEVJ&G)p%{Ra0UYu+2`Gt3 zGXA2o9$~;v1b03m^wkyvGl19}0}2q170L#~TcY1T;AKy_cR6=;YnKkRmnOofKPdzwk}uc_D7BsR>w;b3q!yt~GQpZk z3jnU|=feVL@Y2uQ|LjRVck>e8b9m{W)$@JJ;qhO(+Hd(PF}wOXvP{foDhWusIy2Gi z4q4QbAHb2j_X&(DfckrLB^hNU*NnP`p$emk^Y1@{v{tG#Z=>E(P6<{nul*^TaNO!s z;fYoonuRMK#0epvh{k91dffT(`L6uW?|1?aI1gjmeW^P%r@XNIOK2I5`|PoU4&XY= zJn%qnnK^KcByEhHn58#NH{7umN?HW^riTkdNoE*oWjavWB3lUZPYWo&#AJV2p2^{g zh;k3_Ni9|Mm?qB{?aEa1p=4ZnC1E^&ydTUEd3Yg*a%!(;_@hxdu4h9E*BywM?^4FfQzyEj(E8t4qn)BRUn2Sh3s6w9VJvFMj4)NpN1~M zl9+KA6DPZPk7-}u0)&P}$Sj1?UfXIIM53PVF2OtWGgUhD)8tFY63AhVwIM%RHMpqD ztQp$2EpK?^>KR|N{Y-v%cwfdwvXS;`Hnga1-U{V#fKQ2&i zxTk-y>;K)#ad*O6S+b$-NlJIz&>AswX!C|xxWnczDcS5O>J{4Kw0a##WSyb30xS_3 z)Uu{FDht&JUP2_{{ZouUp;)C#_<|4mUjkq}ZK%|yO*8lV|D_Fbh#@y&q3hE%(vWEe zqeMjpW$5!Hn}7u9mDEEy%U|nk{T*+JYXNq}+AFJl3x`6c za^BhtwR2%T@!$CIKs4Kc5f-o^31zJMkYWOHB6AXw*?ABN!IO)6|f5^q76MQ zYH~E28zISZ)56M*VB4mJoVE0Vf)>E}LUfS#8UQ>3eT>6nQr_m!`P`Byy?Xp-t#KHq z1y!wMy*gKl_RtXyoDc}Z2wI>^a?YKa-OhqF*`YZT0hbdf(o1y?tt-et9_n(E5D5p7 zI)bA|o3bv1PR`pl;yhp|tXRlrRwjfu{(L$cVnby)v49 z#lpo2Th$ZcBJT&#p`#*QRB-%uM_((BG+`#;F$^0VN(cf%C{0fN>(aXDxJ|=osC2DM z1}OTgl1#P|M!FCT^{@%pl>f`<#9C%lyL?%g*oiGXy-re=fr3E&97!S_*mp9Qc>aO0zNBJdB{}JDSB14HVEN( zPTvHCWab`1A_Y(qq}y)pW0th4#$}-B_QJFSIuX}XWzJeT@T|{RKl>*&cQ^h?VZ*ME zPLIM2VNaVt=$WA_YQW6{aDh<-*3e@)B(?7wu6l=0x)zovPR2h>!*oq(jcNcurAw;& zs#FKHMmGRXGDL}hL4=jZ_H9B{rySif8*3#ZU8B@MBCHZ`li3xbS~jx=pc?Ed@7q2; z*9$1P_;6IKaFEJ+DPKI5wsg}767=U7IIaTHDOE?E!~pZU9&+AzBK1-8DH|dTgH*ebwBSf)2l3Y+=;;o zDz8iGz+&}vJUAZtOxrlsG#NlK$t$)H%2<{G!Zh8ah}N0#)@l(UC0c3BX3Y3X+Kc+Us4fC1;r4 zHh~Ny6@o~xUIVMasZCy4wJyZkv2;e7Vlk`gt{#0u9@ng*O0WVOlMC{{8I2xMH!NeK zMAnx|Wxtf)OyOa5h!=mU@0rZmdLW0LA9DA*dbhgB{5z{wp$;t1Ll$t|BuTh^9&#=L zIEU61GKezjC0qkmtrt%UTEifDSCZ1@MgPARY~NPw6dx|*H)UC$QP*AX784~$e~+_((*$aqH)FYznA zL{QB@GdYt!8!;zhK73zsJ|aml5#gS}Ey!WiO_xpQ{JXvJW@A~6#eH0DhCl!STB#yZ zBtgpcp)&=>6eJkYZ(0>G0F|EbFEe2&U1?i%cc(PwA!QQCwYB3n6YvzSumlfTO>a6ggjSaYpp$@bc%k))4mn8pZ{2cm# z2ongZ6G~r$rBJy5N@p!Mw0v@md@`G2EC8lP-@1(63aDyccret?=w$Vy$!idwj*q{7R;|Xy5|d z4hC1(r|}N1yjeY*krN6$3gn_DVT;2t(taPb$k;igu9-DY3J0=-thcUBDy0A`ZW1-3 z;>aw=p^SNz!YD)0)C#h-?gp>P~t$m_><1Ry1YCU)EuK9I|*Msf{GmwQ_ zr$#v$9uGy*hXv%dhD8Pe1x6e7Bq0$^;WuVHZHs4UY>tMt7Bus?Qs3#|%RecQqAR(4 zihi;d09rv&$*5*HJ>|NXS%Yb2Jo4!V5hY;ROz1j?Z=nxp9?`y)W@OK&Kn29bc$-6) zsv`Djnv!PLVb64%UYN`GU4D4TDV+lVm1!R@t*;O_e(UV;tY4V+G=$1-E!^@_(ut7( zI!|&7G-#r&zE8@-F~~T++(h2 z5P?W@TenLM*c8v1GI9CxvZ$&+j<(Cqf+J~!LLwQ4;^c<5t(jUCfh=Y2S`U`A%?muI z^tID4CtNpa?n@jB&8tWzvTF&fIX9ExB+2Q(Nuqrga(iam1GCqD68E&S7Ma?<653B{ zb!0Y}uB9NcaHJ%Z1QP~_yiEy9K%Ou+1oj1zFo3U>A-G>F1Mzp4s4fGqtj^-_TtmJ{ zJG6&u>@l=D4gX)dd3X*ORO?Bstmv+hAsK~;Axhm|DVEZuH=aZr1(PhB z%`@Vnm1l$8tee33oIZ0aIKv8eLDQp0PHhy^U})7~`vuf!+_063;? zk!oM_PB-7>C}-aG3nfZ{&uUPh(k(7cf~@GC;gH4xP{R{&Jzps*H2g4#SfQYRD5K2T z08I%T%C&8|bf?S$OMqx(-srM8V4rU{wC?1Om`2Mg+QXPCbaZIzWJkG@9XkL$K*GOP z0!Rf_g`W@DMu<`cE9s|`uT`Mg}wj$qnrOYUzR zAs*tq4Y$i-UQ*5yzQp6@Hj1j1t2#P1t#JDARU#L;G4AvLH!peammlT%H{g*1a9gbw zr>|)OY9N#hY3|^Z5PiKv0un)h(j4NjMwCklDMSixkl9H>>{sfC9=>{$rZmPS>j2+g zTt3HGEz@ybsTfG~C%;4kG84pc(D!h>pw^69KmlD1kWYE|2%wgCJD2v8=|Rkm8{LF}et4XBB*wY$%C87rV;T`Zbl}GroNWXNS*{9jn^t z<+E9H?SFjeEIl!yb%BxGmZ+KmTo;nq%j#4fsYZtN^2Fg|j?afCcX8|3e*Wn{^Q)J6 zy3s1NyP4r+63rxdH5zSV%p~WN^Pri~*G_<+U!SAPT!~nY_?8-$CXfs%D|rIZ&$=S~ z@4b$vW~hAR+3(!4?T4?W7?$Rj`ueS6yrnPFUfzm+Q_ zGjE|jtDn!K7faF$CC1pgz?ks6EYkNpRfUt%{`|2ThxhwFu zMr5&2-0FtR_mb)u{+a0h9olnJt{2UuU9Z4z8$^&+_~)>&Qtw zJUAoM>^|2k5;3{Acyr_%kcZ3mqMBSK5O7dMuqDZ$L^1^~h96tYz0>cMb-CJPhOdGi z)lQ89;5KizhEF*6@*(XA7&<=<|Nmb?3Iz+R1~FEZk~Hy04DD0SV2Gl zGt{O9{q>d$M==w^IwXKg)3lJyKvsExIEx+e-Xta=D|Su=#s~ow)txT9P25&*eM71S z5@hdhc+TwsU9}iMtjQH(z~=_Y%#i0{*iFZF&G30$B4EsyEVALQi5Y-e3ip!f`xwYYjt{8~>U;BV-ipRs+X zz7PMx73+8}PH~9^665LuwZ3uP945tLtg4OiXRv_8IwtDZb{9| zhJx61J>q#hy%Ez82d^xTT|LGVp>j*1c1o%tk-ykGr@wL?PU+)}jZvrS0KHVdp^&YI_D#-TYohe2a9Lw!>8bObVzVrV=Ol2|#*jK5|%19Zy9Ff6QNy*IUjXmt%b4 zel(JPPdmh002?3z&QUj!T0y^D?R67m0PI4ljhX;pPXF6_^MStpa%vfcRDs05@jaKp zIys#53_zae6oOkH3b|ESsTK1z!!=QM%`x^ntHS+~(wNBJcPD1f@bo@Bgj*`c)mk;5 zwx`K|LsKSX6%#o6e@+FC+Q_z(%?DIS53rJb#|#9fg6`RgabR8x0j%6$rf&-BQdT1T z;h94W#Hx+FlXJNI+69if+2PVD{v1PLT6lXmZ|jDmK_QWv+c#e2ikmXORGlVJWYE+c z@<4zODDA;hK|`PGZTj`TZ2jypy`z4~vKak;^R?3DgUr2pyDxmak<-36r{I5OnunGG z)M}^z4iEq_<*J?#kW@Ep=0 z$YNm7f(4La3ysFH71=D@UoHalfxE#~4Qx_cc`=}|$IA95r~k^UkLq*^9PP4`|B zn?>JpTmfjeAD^MJ64pDK$bE~y=mm&5Ej?|z*V70~`T>z`1Rl7~Hb{oBp!-ix9Y3$e znMfs?PXPO|T3+run#^Pb2`&qs+&RIbORmZ@niBxNqhW?nm;i?gjW@3PINcx|0`@!Z zi0I>ZhQ+cR7UQdZ^2<%_xDnm>(X#S|FFJ$*f*23fgNF&9rvsq6&RJ&M=op~H-g&+G z;_2=!IJ}A~LLvujkxhsZWxY05Vfhd^L_4E0O?wkoUrJjhVAcs~V=9&syFCZ*WjO>M za2UQ8_9m=q;wA;Sjp;Px#8xzsQAQ6|lL6@X$QWbu4{uTyWk`ANol*n7`0a6M!Yxm3 z%Hpyf8|I)nL-J2+2P*lqYs!jM>Fo>bT>ra_l|~^k2@=p}WN6*9Q?Ywt3~7M?M|91K zRc4is|J}bZf(a-njiExJEzUQKj~gzW#HYA!fVppN?q(4>%d%RLYk4M$io0UiEr^CB zd;&S-`Pwd;I&8qU8t{L|u1tNDTHd}!-V+%Eilt>#TLw@6n0I{b32s|89E$xZ`M=Zc zeWjGf11+keq6Bao1OCjKQ()4{73Z}n0GcKC(aprg#YIZbheUxTj(dnyM|)cgf(h!U z%Tz?AFQK#m^SJ(u?S1JM1Dpb&Vh;OV5BRCGaJva=4Tmx5l#Q>jfFLJUPsc8D@tpkQ6=L3FFxEyra zBS%+RJ63HM%onPpiE?aS5Mrm%q@KAs+jB(o*@2*#z>t@%iBZ!)l1vbGBLXy_rpj6! zwUQ}K1LUtgCBMYDbtJ$H-V~X8;*^ju{~?nb&f}rCRrw%g_xQ+r*(A~B=rn%EE6M-& zKjW{Np=<#?yv>*a&Jt&n^Qbw{OlA8t@a_7;y=n*~C=hU(_E$5SH~LJUd-1{l{To*d zxjlGeY4f?S8=xLoMYYBNwaDJJnrJqF5w6yqD@vfb6hN}Kg>kL<;tB7KD6GxfAZlea zPGd>t3TzK3Q?#T}EmY*zvLxHaStjKFl>n@*P_=Ka=TE ztE3RgbIEBY1OoFpqlABZK`nz>`1;gs9I=@Q)jZ z-!ZSS&V?7-ItWVul!g-;goK@!=gj$r{k>ckeVSlQ-sr-e`!~Ah%ZakP%p)GYMd{{! zyekI)lVkE0$2+h@4kB&&Da+*$+U=gq!MyeS37cjOotI1!QOb@7{{<)F978cC5E;?y zC0oadB^fE>^YdNb0#w0C@q%IB+}65X9hVP_`7z9o)V4_8K3otO3T{2jL+=6<4cknO zAV9KJ6_G;L>8C|(2m<&@GR;=yL#-5)nQHtyI2n9rrztcs0Zr7zgc&Hf?En~mm!NAj z1|pe90ADuwXDORV?9EKjV9>6m#|lTQIrNYgBo9Q}U9}PA6t*CC^=cYwVzxfTG4qwx zagir>t#-5$ltsYAMDV>-g`n68BsEbLYaxjUUKKr&`_rImHk49LP%p!&+Do1(C$1Dy zDk$zei_c}1|1Mko=|rF<(*>tz$<5M}C?eMG0$R(owp@@H>tR9X8#^#T=;$gDTL@9% zS^=yOfYyk@T8~4S4^Ec~W<-G2*qUiKGB#7oMY?wB)GDNXg(O+y)>gzJmlHr%3^L`k zjesLRdd1bFNRU=dx9cwtqXp0NP^qL#wE@W(083TqfRMH@s>3EJCNS7zf2-(U*ilG? z1@#MOq}F|i3=?l6aY@T*LIy?frH>A*?>H1()#3zZOjH4_v{o9~oLWU=Jef!Bx~n}5 zoxD<0HUcoksRYR##jBxBn1BW%Rj4XpB%l*SR$&1u2nq?x5(uo>b;5}a5kS<=>xrC( zm%i(KKwHg_FHQ5km!0r-OwAzRapmz8o9K$80Erkcx)J%x-wQiDLiDM>mYIY6%pxywS3z$}B3A$Myc z6daw-2xm9cg3dLxkJZ{{I1?XZ z?)E4S1uHIYB&nb=UbGYe790m4Ml5DkF7p5)19j0<4pjseB0)OH2ZNm(J$Ln8#nH|5 zOP(FTSM3j#5>szm!LAFe>61@ad-MazfyjIG!3uIVGhICVqH|3Z*owoH=Mg#!-1JZ;8rBoliR}_?22(w3b znZEP^p1lHy^geljhq}~oB>_wBX@0kw42)CFOCBDiV{EVqH^&=F z1$v8SwsyZDa1wGm(Z4qxelSwG(U5V0zaX!K{5<2m!BT5;l z36S(}>emoWRGTnrGVz*%G_ytDQcVk+Wq5-Oc7>hkk3M|C{ik>CB+CNIh~N$n z*=@hxwamqz*t!fnuL5^!D)~XjDF_X!{eOs4ku217AU)ub!@tSdk}*5J|tM*?u8C__#trMJ%8hJkY4iFY{;j4CkX znfM6nvyL^OP#~#p6>FA?;Zc^CXsXDg%|4heO6?siR?tJ3;sU2EhH;tijz=nOy9ZSnDMO-JM^?q9%`L^Vp-Qn zGGj>f^h&Sa=|?Fs1PFHdLq=y{!FwW16={~3O1FqA*s~|1`4^50scE{A<7|3n z5Ic=bz}&p-Q8F7wV-FCy^XQpqX3K-O*u42ef?KWC)YK4%wfJ@mZs#n< zn!*R2{I1k~-csMzY-1E9kz#~}l1Th023Q(qSs<>x0NR-#6C~C$TIJaHbAHk`hd`}9 zpFhbwV1$B=&yVO%c1N-u_uiv*UGbm~j|MO1OwG8~k|$bo%2JI%m$L1;czmBz$9Fh> zbnjF1l^e(8iH?D+7NlddK9-({i3?0%4mh)~#4y`en3&S_ZyZaiQyP8lx7tk;RSYMI zD6EY5l{XrVHJuccLSzKC+KeuKb`bWbP8rEv&ksvwM!S-jqydppUb zXcNK6R84srA>SJq(~v{B<=!ar5t3Jl6_wXM#sXYB$_`zUj&#)QO1OPfPu^IQ$6k9< zHur)mb{<|KRug@~?|*nmbvPJ>Kq;8>0o?GYBt>JCi4A+6ktEFcbLno;b>S^~y^ zWsi^4O~(XGUshmbj@9WqpXy%Y0l&v-X8MLBZAg^i`n&7%UME7!h&8&fhTP4-$A9u8 zj)9A1{HD?Dw>8b~51u)%4hC9Y)B(qI^iS*$oQr9Ul9*TF(b+rzof>(oI(Z|sqPF*) z*+{1=tE8$0J;r?ar^oq(Wyw`wN1%ufmlfIog9*K&$|RK46MAl6S(C-;5$ME9zW2wT z&w?_lpu#iVy_6HDXP>CypJ45GklenGf^x>8lIP_B5ZMWY${@b{LYPGyN86yctxQx25{yPjsi=JOT=-RU(Pg_Jj3G}ZoK8)L2X%9zkQSAOVan10=kPZA2O*W zBpAbBpyWHD%e87uOu^N${>@GeHR@A0xtTnQ9?djmsox^@E+N`^Ch9C}BVZj~@xkV$ z`MEouI7xLUyv}$|crJ|T^*f(9{hairkM*fG-5-55Y!`y?jj45G%YaQfYB3!UN9x(T_L81hzQnDIB~_*NM1xlQ}jKx;J$UL zbCxw2;naUlV0ta%*y-=3%ZFC1nEdg!{+QH{nWN~TH`HYb#QGscgi_UdLYJr@ej{tL zn4hSi0U(%KTPXaydqkau&vkgbTg%$)ZedTy)e67KFu0tvc3dN6Wm$&fx=Xn2O{L!l zPx*6aW?)ThR8>R#XQ1{cQMNf+Sa0RH?c}<6`!IV)#ZJ}5$CB=fN!EeEZ0skGF{OQb zatCM~TD3n^>aT#c+Y@qrT5VV;zkiV9fFwT4lI)?WRvRZLkGlwu1nu76aUW+XQ(?(t z{n5(@D37vU9upR#OsEGUsf(&sqpKz3?C;=raGT(fN#=bZsz4!sZKvzYVUqM+DW|nR;q{npXu`EIIDcSUU(E}!rv@3ahh%d%Of zcD~^LfL8Jc-OZ26%!0;|i|_6~f6qcS_LtGqbb>b#AgNe@sv1PwWtFBK8_7VD$_J)` zpaqc$rh#twU?Gx}zhL%*8(&}HaEdW;MNji+rQhBjwDByEls_mM-s0f)g>Jg~7^7eM zf;aAy?M8rcoO3AC9Ufo2(h*INoCwG1J_lB_inY0UuhgdPNc1t|No7}~FPyyeR))QL zKoxqy={P<^5?eHhodOfP$qZ2d3Zs#*L?+SCUm--@(Ia8j{G)4otmABtP9A4TPV7eB zNQ@Gzc%7s6@P{4o z3e&!UjgBpvKlf_k^ahYXGOOTiNQ_sy{aTQ&Rs#gPujwHc zqsGvY=P=aU?E7-lOg~yE_(h87DEGVh{-Q0Zuc(@k1|>tjowN`v#i>e#(>jiWquIz|ESuLk68)0} zfs(Kkh~%;~MgcLlaI#TWB3f}BJ$!w6yu#HF`;7)8PH#NLo>%6~eaIka`0jS4^@JvI zn#_u7NL{Aie}~2b=nOOqO#(o3Mhy6<6*lld+x+=c?)~z1CZVB<1;(M1ulMr)SR1eT zicwl`e57DBx4{U&CnF#!Go8QYF|cf}TqBIGbL6l%f|;y7pEgRw^sjK{aU@rf_)or~ ze?UMWfvtVUNJ%=l;N&gmTV1Jxsm1!-bQ#GJitu0zd(!Qa!*((?vj1Yu>ci=aR0IHFrs$dtUE|K(3s5dx&tWmeEGg{8D z{j?7IFMRLby=~ew4|_>3{5=5BSjJ$YKn;PKNdiNDm8d7z(=5EOA{7m5crAH(-Nn&k z%#FOoebEB6aXEQ}y}?=GI`Y-fVtmiQV1?R(kb}pZG)SN@wf(`foEfgQf`>r{EMA7E z2NTgp8auw5Z=31f)5);q5{MCal}X=MW*Ua+0TKmr@D-@;GL6(}U`D#c<51Qp<@>}# zTn3Did}s+gKu-qJHsO1i@!8RbL20gqT$12I78^`^n>p;r{ z3C9G0s6b?3H_cu=!`1(F?|9rO0zh#KF*7E{ObwRB11hdP4TKw)Y}6uR0LAPt_1M)& z6KUEXt@D{6sUAn(LZn1jF3624-p8CH3R)rlUyn%u7(cT7v1?vz?vUn8}*=5A&n7wA%$Zov(OOfQagZ zkdr{E+`E|nQq}&fR+E5YzwuA|YVmLQ{z8Wk?fO}&*x>xxGtVqn8d=FOPuaMBhwk_T z0z?&O0m-3oI|*)QX2$@KdFePV1x7}IOi%3STIZG5WN|$3fy7);I*7(@`)S#>DhAmv zTQRmvX3ZBC6^sNYaqW_|YSa`2!a!{q9gPiJUv z5JyBZjtdsXZOg{!D_w^6{p&f8a7gK)g@mY)<95e=*qme9aB|k3V@63Jp)8x_R}zdDx1<^0w7P^d{VuPyGUT-nW&EW`g&g zvP)wSb|k3o67s=JIaGBIz>N9=F!_!3pZxWuzxb04vba9$ez7IiS%asyMP_lh@IvI6 z+c)VZeUn=z43M*`qAGKQ^>01pBv$1VfHJZYOsAJ0*j?Dtoy+jP1_Xkcb1Rp{P{++} zm;}FXPZl%gekrKi*ZPEZ}t%&^6&7l?Xl7g8>rQL;zVc0>)1fI-HkWBzMDU zd7A-@#;K&5+2v@T_PWnpBAqJ&BozV3Fe4yf^P7|Xnv*%2CfKYg0+x_5=j9hmH@Z_I z2ev4?t>PE`?rG|n2r-Cth?S zB(bFG*JxZ^KxLFpAPG(HgnC#CMNZ9*+5YW~h2^Y`Ns8_ugGB(MOS}l9%#aMj-WKh` z=4Aio+Uzp7IBAax*uW%s>SS3)p_m~?w0i)%aq?zldLzMc|nJ|csRMH)2{_Hy7@er#~EP3GD26=ZJz9Om={<99ql=Q zKta^>j_cFq8w8w@9J z9^wK7F63ZGc>oxgnNeE+hQwt2UlPqW7R~+!nFm7|l?|WX&i0oWwkl&Hn>$RV7aU#+ zBnr5!n)DJw*beLya{<0Mh9rnTUILZ!(w|9GD%ZUc=Rgtf5%iavJmEfZDu7DT^H_aLlX@cyj(m>l}kW(pgg@7#19App~R7Q+@ z3II$CW`xL;7!6h<%XtFQ)#>4{PtYITscbeeR3ZYC4eDKUu$fB3I;nKRYW;l#zVCAJ z`g?TXc8|rdzrmiqpPKHu5L7wlYX-2s&Izs0I=aRktiyG#HMI@;cz1bl>0;Cb0!b!r z2b1m0(h`HCX&NB?XC-Eq7gowQvUqdgNHg8ed-Ul`XWaY12$Blx zKy?Tzd<4O|M0{TWkPx4Qo*88YwUfa;vM1#Tdj0^>NQwmQ(u%9u}K4I@9#LOleM0q@fGi1{6(B zP*9tCvhT@VD?4smQ_2I=03xh0Lj%#x(vt^CFeU~|*ag|tQUY5}qkREHF_5De$uX2h zMQCKMgU7Gti-=gaZViQ=VA;N>!tONsnf&ex{8ZJzS0XXTj=JWQaE%3Z`mJ>QLH7f^ zwiUIl!j)Gxp^V161Y4O^J({N&m)>q{?_KAR2X&qHaFfy#D>N2HnXR(znO{XO@UG;0 zZyQ8dxPmzYGaeAqSwR$XZ;P;%db5&5-SGra0Q`2w?BTP8(7#QFTNX2U+1@TqG#i#G zg?Px>m(c?cc3gZw`v9eGA$t=c1jeyimE%I3=~+L;+EIIPAC`(bQ18f2rJli>G>&(y z(!s5nfcL zM9pAKAkDzEjT2#G2u3v&IOqx^GKc#)_SIgpN|Xl9NH$uq&?X8c*&UM(V{2u1aOp9F zlvZ%5M^P2BD+=6k(E~0J*Iw&in`jR=+^ZU6aEYlMP_p{hG^!MksL=+QzOB{~gyRp> z8P^vAWQRtqvAeo|GdmMS4X%OCmZM9z@A5_ur3Pzu&*sdU?K(*ep8dJ(=QN{TzPDd{ z=!6{yS04W#S*7~sl~ODha;OvHdKv{7n!yS6k>G(KbS?71svmF*X8 zUojan>d>;fRg6Nhhg+dJx88g4e+woXY3Be<%UudpYh?G#` z;d}33>s(YN!VuPN>gHb1n$fkOX>sN<%|S;=A&vTd?8jg*wvf`eWDsiWtoGTS6D7VY zBGQuRO!fJGX)^MPsM>&9!bm#uwJv6hP`~UybHZwHLy}Lbzo9P34p4q8t3hZ0&Ew{C1G!>sP+(-w~H=M zO;D+41e(VyZ{BR4LM2tc04a9ii5X6+-(s}A4%3VOi7Rv;0YJ1bzf6#;&#LwDiH z@F2+~AW|$bU+8}vCNiNu2oEu-ABv%530lqU2eo6MQDuoqxb4UISCUmOS+~NiUfUCs zGf~L;s0_1%22oenGSpQvty0s`ryC#=IH(a_E<~9qVegux&P_n8!Qq3qOP0Pds0Sz( z_T^-7)iSuXl7`wA9WW)T-QlQ4|qf&NAUde|*xsb(yeGDjZtI7K2dtWMTdDuO~(YoCwnEKd`2NXZB+ ze0}?sr(320!T zEHp4V`CxDhK0J@<@U&*KSrh42qSc#S0h^Duq=Jre;q}1S(ghD-T&9&Z04xpHiPFKx zJ3opJdYHBpgW~?%`Sk$mOo=-!y4Vua+CktYIQYP|_91)lVVCzE7}*lGkF^iCd&hld zXI|3T<1Z^YGcaKTr*T-$?`C95`nzW!!|@}(&T_iRHTlkuczV4Hl(wKbr4&3#5Xe|t z7I@RE=GVD&LZuiPzw)n@1o|A0=F9@p0uW$gP;U*qfB$o*?pm$h_;8=|8E1AoYO?Ub z$DNlQB#6=b#~T&U^IXs1*7VU5mnW*O@I`wSLPFC&Yic`wvfcBU&4BVEjo*h$Hqq_ zy&Fe$SL!x*_R%}6cfrpJPKB5$?!&GBrdwY&Y7VV9w|M1GPVaDR{uLQgmulLizOt;A z5Sr!={j1&G9xHc$5(BHPa3f4290jbgS$NaJ;id<4vm36pFIFK&lrWMhr5>OndfxGo z=G8u7i=Ub3>f;`)_Z(~Q{bkS1&z4T;uYB0g{hX7_PHD68USIUgy~Z1+*d!8-`=%)=d4aQ_z9V&7>y&EvihL-MWH_}PV$#$J&tVNKP zyFsTQqT!9LB_(%TB7qatH{K@nK?j+j`3l!L?wm}D?!fJ=a# zzY5$kUxo|j9GZZlfPw~xG>T09)6m)A8HEG#8($1_E3IU`6I+=BDzbZl)i;e}7z2@6 zO-cnlz%b6O=p3(VQ!f;~?(@+*bMk@XrdQs1T{3mx zbVD4y|Kn%8i$U*4e#mRg{l+5?Y#Y>1UUK$-y#)U6tA9MtXu@d5G|K=ZAlW)cfbC2B zC%nmx&FjMuvaNA9!cnSqs*2=XwBB$W+lfXK)x4(%;p z`4jQ!;~#z&gTGJxG(2$E5R8?x*VJgTf&RC@ZprVos4`y&-$FlDn}+D z6UC5DSWEUgM<9fK?yR-9gLaE6m?bCy7!JiL26$NP*fAWXSezxh zQuWvEtESBWW!Ku7AIeYyaK&Kz^cjCFH4Wu(q!K8sm|-BV9qnY`D?FEPiV(-xg%B_^ z{oQZK2h|UNX$_#U+K}Y?mCfU#*cF11&X$$N&8~p}6A-3T=u_?2GP9wygw-V!|aMR82|xl753r`p@{KupV%7 z{zCYBa@;Wom1?0(titQJZ3N@&9qwqoS;B^L;0IT%fOTuqWXRd#u_N!h}=O7 z9F+`Lnn6w>16qnmz;#`wror|8hk`?sc`}kLkjP#`rU%kanrccWf-wciZLX8^kDSQ^ z45YB_#=ySK_Q6D0dRh&mbor>NI!~iU2w3Y8wwRqwO#&T-+=VY=c69`Rn*D7+YPO*f zQ3bBN;{Ul|X!X$^!tN8O`lf!Iy|ctQz#4{-4D+k(mMgO?q1I*OkU)@D?qV|?a{0bn z35CJJ@=4P{k-})#xag)Q(KIzP_eadUL z({&DO0tAr|xEO0PB%l5I`cWjlrx_U`c@Ers0c~00FwA>I|o_&yq8_K=osX zrab0sSY=11!qU(n>orTQQIOOXxHc%5!%uo;d_4Wumwox4oH5#7JnAGJGhs=RiPzSO zx~BuBDc1qW);A59N-~*E88c)8w0#b?&-X9;s`lga58dYKr1Ypp#Qzdwj#`29P)d8^ zdoD9iqp+SS_S?cAef8o`b;g7QC6pXW18~1Yqe%sr0F#P)-om7tSs6ID zuGZF&v2+fkbjFqK%1L8jn0ys?tOifZD)_+%bgY9@Oi+GP zmKlJ6>L7wUI4X}lwLNJp%CO>Ke8u!no*NpkTsLmu#uJA7uNMW_-!{a1tTr-@3?GWg z587(VNvZ8oE|7!^TIR3^V20gGrV$!|va=s=9!00$Kqp>$w6d=(fkY$)5g^87;l`JM z8Cns6O`Wqo8O55N<(dHi1w;T{Er%0XEs!W59@@aBBE7MCFct+Q2?yLdaE3Iqun(5~BYLbkmDrXe4#qzy%e6yUskgY7$l~1#>QR)$y=^|sb40mTf7EVVon_>C}H5Kl6s8A!hth#FL_pBuyo{f?#yzKoe_Zr=xUDH(&L4YCEM+g2cFGGyM27Sdm7R7pTk$Dn_p4lKmqHg4;VR( z#%#nP@S5(JF~bwLxsL?!f0(|gwx(?h${?UXiegQcP7j{RC3ydqi1+=W8{xgA}CGm#Y`!WS@j(T9D}clXLAl7(1Jx@ zSC*jqQW)Q_H(>bx{7X*nF^&q5fplHm*|=B}fp_KVM&y)CwiI)Pqp4Ofji;FCsGvaf zwxw;^VHJ6M1(<@_O@qThyEA-$9E(olv;5ZP77LRmQv>l7gT2+K(no!BNTl|KXD>JX ziOHEIvWrf4ANUQE-Z4JYFW5Zs)s!VJ37|L>)Np^UcLmub3AP${-A}blGP*TWAPGux z*L@HSz1$>7@W48~j%Ila04#9WT^^Z!gtyj2!RPey>2(kE1aRvEjyYMVw#M}M=#xEz z#Z~w~nz3M;_cjGYSQO0k&3&g#nrQ81uqNOST7T`e?OU;x94-nJ`rm7x5P%6WqNFCM zIs9Jr?V&a`MzUThy&j`jy`6n$)Pa7o&eGb*J^QN{j$7P2dX=IofG9xwjj5HiFHjF) zC|?=12)4wk3?z+JW5ImtD#nZiz%fa4sSOK)(&#Iqe0UnrE9f!Q6gvd zvboWG5nqiOR6_ymI1*J=kS!q9Sv%HI#6<>?X;4cOOl09l9vmB%1lI+9YwdG>RlW6L zKT@ZAARu3m4Aynw=wCQYb22eC1qFapKjIJi z-}5^ioi?88mfScG#wI8MjH6irDCc+0%-j_7 zjR%qkP*v$^VIDM^Hxax70trwKRI;Ny8u=)8^z5Bi$(j8GG+bNCmiDpbkOOF)+YJJo zNg%guyLz#2Qeytu4Rn3KoYiqUbk+9(rKs=T=~(F*%eIT z>d+nTD4n~%WldDw1c!0B+pgL9LHlb$;oh-6=hC#$sR;a=XB(IW}+Qu@8WbZ2BRGd z$tI0XXGiN!tm|0OdToT6L&5A!ObDvlq-6T=x$SNY)=z_8{<<{ zV69GbuAa3EB%301d|;6b6bf0Clvn{f7CU$DM6~y_Km6{53P4$R!unvz!TxbBQ^+BJ z2&VB#KX3my&s{aC9*MI9al%kssVX!$Hq%v3OiSqn&;M3z$pP7mh;1LdipQ`GdicSmXP; zYTdT}v+mc;kKox?2Ot2x0vHI(g6)nLw|%kq$Foz~-Vk@nRDITZssE#G$CfMxAh&`X z+yzG#UgmOho6j$7uonUk3HyO{aRjaj7YhLV#sqV5l~eS!LN0@R-@I5HyYFUtb#|wZ zJGTjtL&Ds4Qbh||!59dTj9U>(CeIjTihVBEh8U>z44V6Sy!!pzh$pg|bRAg8j4-<& z?4lVt0|5~*tgITNj<lb`$ip7pwK=r?QcPEyO zhgC-v8oOD!ZOGOlc?_VQp9Rd{MCOAbIt(Lb6mK^NTzmhaBmsc0R5!K4I`Xg|y#KH{ z1_~5$)937;^hsO2tul+SP_6eF^+DZ*pbhu6LX< z7-^t_++Tj1TWU}C`F@jIw|r2l@aA^;x;UGeDg!>?N0thd4^AVdr8pezi{2*Iv6_F8 zy-&?*ibKK>nNDWit48*VP$q!LtCbJ9Ja~HmVE*^+$e_XqHtJ=0tSmui*38UdFjMg! zhe-tLh$xIzyPJY{4TF5^eZD(SII&;t@Q?^qQLrKy5pv?!fNFFDQB>$znXE5>@ebzF zhujT-ldGa&psI#iNG<}$^%f&)N>P}7SRV`3Ti{T3>wSz&cUrm<+!{j+6ARRunW=QH z-AI56pvqa_O`b*OV}(`hG^H^<_t2A>JJ0thH=hT{2+UJ$f8%F-QNuY}Yih!*B?gDv zy}BNF)iXoYWIgBC_Fx>(Wj9pS`AQb>7+KiT!&rpnDP>2ES&o+3-1FnQ`Be+wi8RZr zoT49$9WmNH%a`NO>sQ30Haeol0d;nu7OgPmYMF&;o6)pK3>eZfj3?x3MKe@1hfY2^ zyT_eL!eG~QSLRtrO^oglhAj=$D1^dP`_4N55(EVJ!sGigIWM7!NCtfM$cu&27~PTXk!yLSvhYgA|x za!6{X8pNrlx!u!#84tVelCf@t%MH=T0(1+}{g0XwpVpG`lLp^_dvS&Jh$6*|DZqiGXoh2;4fo6WuSnBB}3V46z z)nvp9q(wDh^rjYXHX#L6p4G{{ zoLFX6Dm7GAWqwBPe9VK2j0229HJJj>vj|9R(zv~f%9v9Gg45^j-d|W6^KBvkpitHOT907c9+MJt_1ZAb|eHaFSpDCvz6bT_@ zd^)+~hyBhUrtF&m0AfI$zaUjZ1yI#XVrKuOpX)ciWh>iW0zt%*Ygmm$MvK&rHc7J# zM~b!2o>5%9Q7TLGQ6ki{a_tTN6Fn=(5j=;4PWt}J)cW_xucHD0DhwaAaDR%S;+O^B z3Pp~zcC=1}f(0Z8Mzg16!`{qvv|e#JPcS-+r28!RM zw3E++Bq&4aBy-`R@ixy7lIsGe91t;U&+3`J+vwNYIMV)72R$hi4LP8yI0V*=*Sl%^WG{wT+(^VAgPD;qm>|e-^Oh*n7y(`wyq}S& zd*$ejUl%KnA77A-IgNvtv_+lQuxo&t!fG{vN+pk_`e=bzr8P~h;hL;j-#8nJsv|@l{d945 z>#*AFkfRhyHJQ1mK ze|92}E*{F6U?6E|ECC$NSlV~*p7ez{=-CGv9AYjx7Ycw;HH5AY^D1f4WODR<`5oTJ z%=x2<2K1Fz8l@ab_B3uplLG(?&=`1{s>W?GE_aEgZMwQ5a`ayhq30iYbM zjkMuTm{}7eAn(8>!d=|k@-8W;QuWIdZC?j@%Zy>&0lXVBoQ(N?lK_dU8fNgK9TTsL zDnK4`xSHD3hf--7jO$n-LH#O}e}kH2Dvw*iHE=V}A&*ZN_wBW%=pq}~s4a5N5VjQx zEP+j~7NBty*4$0(Ly`DM=s1dEGO;=R%&j;6?caXt(J4o&D@jZPCakDOTGH%Z>=l1w z=3?J{aWUmtzG+bz_CLITg6F3F4)$L-3uH|vppawjqSQsyO-_1}clhY;v?m%~8%{y1 zd@=RHf+EYDW;>e5aVm%XmJheS4f`FeY}kKadB}k*wzFnV*c*A@EkDt{!$-=ZBdzEr zoYnxeaQepGK(o}Sq#AvS5e;ydE^5s)Eq-9}#($uJGZRx)<2bI?jvA!5Z!Ky}168Dy z;#aPjU8Uo0cC}d&UzF7z$u7R1$m-NlIufim=`uqxdpJMxu`@x3mJ&Rmrm+C3=$SLS zg#tOivK$x68hTa)Ggpl&&KYK<(hBuh2bd{E0>B~Q%(6ocxsm%gvwpI9ixg}AVRuP6 zzY}9qDFKqpGEJ{)I;GTK)Z_*l5TpaBol0Ijdz07Mi4V*69+G2_Lf4;FiA<1lsxL%&U1F$!-xAE>6e@W@8=#6nINl~ zy1LdxRUMoHOp^!@gPOy_edT&r;qb$lyVd33P^M5WVrf*{z6hn?f*{P@!0(Z0`s1Y8t@IrX10Mg}mGu$z7i(B)-%R|XA}iE@zjRewbU+FDgxzfVz9h!CMGB-b5TZLKYw>dKp7?X zaB{y$Qfh*lC_qNbc?JT;5}U~Ub!j>3S|N*Z2^bauzLsmOp+7;Q)20C=DV)|BC+r4rO22BY8dt_9d}Z`yUC5i9;Fe#gvkm5%I?38M5fw$%AMG;2Ou zh7wY`SI?R)KjWRx&=|MmOrYXcW5Ol^!d%u{hE4A*m>5!639behO(a*WHpiZ0=3<8e z#&sM+E9SanHoZK?%+LS@fODp~UinML8xF#TTOqm9+1Y8czw3?rYZs>95sz zLk21y&(9fKQ>Htsid+OuM~Y%kSiGA$8e9v;Uf9)s_rt?54mY&vXm~QX+STN^+{t#6UD}#ozp<`YoB5v6KLHj4|jvIvxXzk+G|8r00zo z>vR}-$tVrC9@PY_B*~0@OZFRBGEIa$XkE(Jj@uUxfe+h(c4NGk9M3yXA07MY15aD!rq%2#^{Cg9tHeJO7j>Jzg z!1Oe6y#CD%g;%GKK&F0&RILhA(h-Xxy@owG?=!X|6g2E0+%VHZgmc;#jib;=i~x$M z>0fxufF-IGr#N)oTg00!$k-`S|3}B%m35RU`E7XiCj71GX2Db3BEa&ow>q1Of*g_oL>>z=!V!HsblEK+2I4pd zT$ccrP;eb4#(CG7fbV8zg1zjh?OW~`*EkJ0bxt8!t!d1}n&dk94CIzb4Uq{9PiKE? z$$P!`ug}>B`{Y;(ap+ktO9Y{}*(L|4fV#4Lx0-eG_S>!<3AbxSrKkXh{Cq<{rW^cH znV~eNNe88evv-%z;8)IG`49g0Ne_8g;-)mNY}=f^DB!CF97dux~q!c3eCbyK&DWjAJtX^}NfX&;Pf!Z*u8UX#G z4!xZX%-T~vgWpQ~U7e4=Yy9Xx06A`?!&l_6_a5Jvl@S#Pfp9f3P&1R8oc-@H^PiAh ziyzA|4-Q!snBjuGn7ci=Ljwd`05u^f|5<*%d>vPy5FJeqthHxc#y-omGc&74 zN@d?c3BC^jwj9wvR=}d^@1|8WoZq?GF{HglSD!4$HBZV0Q#s6LUvZnQwXW~n)_3zR4s#coAE>tnH zVL?JL6((FQQthXhdegf1b?c_bz%6EA*d@SIPG0$((wu2H6v%||mG+xvcj>h=mWZxm zA`3D_K9W>l)j19cSxtsgdoSZ9ek(0lcEz%NgFkumE&ki3(wOc*Jo$4Ae%8^x1>uXIttA`_{8V+Re2#A%=&)gB0SK zzQj)@0LSDNydx!LQjGS897>$7s{kv0vm1HHW!uE!`pX;k?V^I>v`v88Z7gZtpM0|W z*2ZWFH9YZuPk=%ezh_7iON0O=H(T4^M+-;YNu*|c(3k(<4-yxo(6A<$5fKUB8|KBu z%&_lX%o`ow`@Z^KoEpdQLC-STH97q$MkB5eB=0Si@ht?1kc@^_`(A0E;qTde~YWB%ayfV&0;af(A$&LKuWK!73wK%ojuHWYrx&Fh%^?_BZQg(i&yf1KopU!AWw zw}=aSg=tQJiR`8-08_9yhMF0yI}0n6O|s^@_gp*n1^6+0=)}kfC{q8_gt~5#Jk-1!`gNdCWYV$tF$s>rDVIR zGNZZtEZX{^zV+;SgI(i02pMs9*e@>;8naIiby_*-8&-m*7 z3x8vI`j>BS_QK1|~y%!ckfRs_V__pi(hi_A(3A=6V`&#p< zHdz9ZHhRELaHTi^2x68s8<+_;Ne0>DSUS?Op{Iw5i%N7xzq4e2yJeLMmTKL)6@y)B znz$|5pk>6_3pRgO?03NSJIJ=V@s98A&e7mBa{QzYy#gC|OETC2q?rcLaO6o7&}bZX z>-@!kg)?^Sq+=Y6>Dtbd-ADTNZRd%J=ekhnBNhX)sYIF~9F#3f5e&Zb$2;kv!jS+i zX%yv<%krKe@VAsHC(5~0RFpHoOyx%PF&{M!Ilv%7cLr*RUD6|dxRYo20VBt?`Iy!-p8k91|5cke$Z31F<~W#e`e}6m z9NJ(P9$Ic0U42HXAO2g<+6aosLzwwTPujg=f6w=#8kCU21rOhUADZTO-WW`E>|>YwlH$6Pg0{NzWkz0R9n;9aNG#Q4BI&iebGFmP3F8v9{q zwg0-cz~KlWB!VGrX+Sux!N^7lyib4d_z@N@!Dy`ILG*s}4_Z#ksRy8Tc;>HpvhU%i zGE?yIGk;Q5E*FJjIzQ%En2J*bSj}_&ftN4{*6coq!P0%|uNw0_V7S3pZt$Yh7x@)7 z&u_^=cK9}g*bysr@7b<&LsM06R_5><)TX+6iPC zBp>qRMLA{73}B}j3PUF*5}#n|PsgSQLiC3C0wP?J1aiiSLZcisP1Sh!M7w(7;#_qG zY>300(frn2Ez{w9YCsaQhAYi;yF(5pL6#9-cG_t!Khu6M!}D43UmqK7r-rLyeDuoy z{^Ifzw`||{C+TahxBbN3>s_>c)^J|60HP)VW9*r^o&Y#S#6ioJoZS-`7y)TMs@s;TS!;Ms z-Tg_;q@Mf7eokU$XGN3ngo`K9<%9Ll22!xw@;|f)Egmu(?FTCknUZR>z;F8o9-rz?+am39tE_-+|6iOE*vavvh9f1I!Q(101@d14NpNy{4+4R>CVTC+! za>;LLEgFGZhHGj?Fu}IWd(%5x)rAy;u^xVvQH&ecY_#N!pxiEe33a4NXvo2tNM7gU1Z6W zhB~K4bV*ODB#BW~VvHIhk;FsF5%%P%0fOmt$PG!}6lg$_E%W!rFHf?B-tFjGohE8QEX)Om&!6jC9`l)%1hVF41AH&nr1A9VBjY2OY?RZi z0#Va_BxsGF{6PwP!rR`Ph4x!vohh3wYJ9n04Mchh$P?=vFS|9E*JIN;E zXF!jHya&d|YtxfTPElhC1u}Q)okmk<%G*^WD-jeChIyU{FS$fB2BYl>D?@Xgnu$rG z53jA%@;sZK-dLX!flCrI+k+0qwrvK(wU(}=+Z>Tz=1j&d}qJqOx58GS)-VX;JBLS*Lpv4FM z?Y`#XfNSole5`^5DHMN7Be~*a>UgSgZH@ZDmm;~ayF!S6f|X9JZ_XjPYnl152|f?t zD5|b}4k?VA9#$>(o^D_6*Sm~z|0iIFs%Tb z1crj5n5ik9-6(U@n1+{}UET6!a(3EuuS5S={dGA9>LAvU94k>-3gP*H`K9h; z&FWyM8LX8-bI1Q3TT!vO9bt}C&#sM9)H%skxQ)pY3T0vTR&|}Yex;0h$YGZ{_>OZ4 z@25w91__D_kyAiG#(PPV`usNUdH#N%x+9P{#b6CPO6<4C`}&X0eLsR2E(sV^!tP+I#Oge(?V_tA;Ynwkj;cG)>elU+-}n$zduPyrMPO4pd}; zNWmNRMqc}Mw&=*I(YO2UMRIn#-a(u|I_8+fWX2;}-LzC*zD1JK!HfwBrY@em_UrF- zoxT-k6(Ta_d>s6$a^yFW?HNPC@J1P+MmVI++K%M;?&v#~_rHC`FGvN7h=73E2hN}E zYo0YNT?+8h2VJ=N!j?=tnw~6Tyg2qKxK)6!#q93D!@+tx<*}1g*Cc8li{M^i6v&7+ z_>6QysAyXj5wzl_4c{~2EA5G9f8=%d7w$i21%k{0fIm+5GBq@2)HOFt4cj&A;eIa* z&2xt*|MKZsMHJ!Z?$gsZ>?q@1DG-_t$VJt3NFY=eD{LvfTp6>YJDOVhFNS2G3Q#}`sj(gMu zC{Yf_bBESz%Q<$!(?qVi^LyES^7`xC{*l!AGn)v&_oagEI2`(wJoHPf^^(d5H6e%P zJ|@SeDgBf!dIyeuJ&aTOE@DDCH8gL%c($)v&fNcGJ0tQ!Xd>ofGMAIqGCit3dh|(; zr~b1uMl+QfK495-8~6P9(H-8AND|PMkpM2Y|5o#$Hh~cpa|cYgoJpZ{2}(d6%7n`` z9CF2jTfOY{wuP%o6~IPGT8QGID7*}@{(Nrpnr81ycCG_x@LgfNvu}I@zG(Nk=r9DZbC*&& z6qK@(Gpn0@!FQkLYrneJp?8Vn$Y{Ys1(nFE*rvI@(SXtlHIay#;0yZ9FD!5S znhzEndT6jDh<34^HdCC*)Rf{%i@43ud2R1`w^(h@a|tYd?XGj_$?e~!``+1#Zi*<@ zQOGbZsAy(!S%MTWE5J6pZgTLFlIy_)05_~8W|y~o@%L`>q8~KPT{4s`!6m>D0+pj) z<3NFc*yb8<{<88kuQk$WH#nmTZAaUDogPE6kN*A?KuC1lQ2m~e;AjLi$`i|@cmnT;kW1Se?S^IIPSOrENfT>2tch;Gf8UbwZqWE>nrNJy06=R zok%CvwXOv4w_9}N=#F=lR%TAcF%g?jIWaxyiSE6Y$FPk3`~-kv(V#L^I(gEQw_omE zCo9ih4}2;3fP+wkMenX0s(jRDr><%crWe&`Zh7`Xui75;tcQa2Px2>mOvIAD0*7VT zGx4;?XD1#@s3UL_VfXxGU3}67I@h|&6G&bn>H-)qqteWL@Y#7FG8tv#f%@FiA<60t zA{t|zv-jcCz2>V=`ITRrbKrrD{)|C;Vq(JgQP7E*@it#&&;P@jw|n23$l6K)j=hBR zAKe--1?;gAaO|Y_0Y%6~vUDL}44W_txg;?Z)!rJF%Du2EQ$_`LEEG(yfaO+&HFb@m zYDKZR?$*J7^8FkCihJ+;`(Al+^WRai$FT0O{F&DJ!CCi*CwVeG)pO->9vw`-akZd0 z2XI+I6Iiqndg9W~d-_m$x_hmeGD;*W zU6P8q1T+rGW7wnw$~MNy{ATaY*VC;onceQH$^8$&muE6bIzh$)juWDA} z47e0Rf^tLWQ|$Jm|Hk(>{-f_c_}_0^rQ@b_@>$vS?gclxzd7a1lb{=nMsbNiTv0Y9 z`25xZ9odsRuL0)%yB9v9&tCcbOd1pLJOO!*2TRdt{}kJo?W4`s1d*fxbxx~9Ad+Wo zoA<-^^BcNpQx!yuW31LUZNP&1y+85Q&2zordfWWv6}D}e<0K%Z5HJJEG~-R?f(-^$ ztH`3dusBixDHy?wt8aMb+I#*;d5m*z7)`_otYkt$Vno3k5RC_2Kwt|U8dVPeN_MDS za5TBs^6Y^J>A@#pev}ip)AjEY9`7-7%Gp~cPO^4RGkl#n)X*5{ZUEYSqsJ9bwqZ7I zBPa!0dLi_V@V074{FQortYIuMZl)4|o%M5k|JWq207G+16iXTT=;}B?#G@j_-L=M) zbv>#8DD9}76#B@WYfB%^H@43H7vj~hRe+uc0G27E?z_0U`O=!T!|3p^pf+$S@;qE% zFq#It$G?(?@2^w_UMHMp#o7UgBm#K?d|ye$4}ZU%&8(_rZ8hlrJ7*_k$6m)VoKR*5^Yk41W`nf0t~=mO^Vp>qdIp{m&V+;H7N#mY`x^4 zxTBL2U0y&Xpq7UPS@7lEYTIu9*3}M@34+6fskOE&mOkwACXoJygShcGNE>&5xJ$$b{#kKQWT-GFq+ZZuomR@_P9w<157u~8`S#!AFkg_e8 z+9Cl^2zWGy0@V707rg)EU46)8!(<~uCxOI5m1|cu+GRs|)rT@!)jdb5#>A~`pWkIRd z;pT~!ww%ryi3TKPglSGe`4W&)F)OK`7IV|Pc5=VZPLDbxOHBHtHrY@GLopTmUxhhuBgGuQW#B(UQlmSyUE zd&Il{@bGN++W0G8i=z{z!jpL-JO$t>s7!{@VBHCUBux%GNsAg(bOk6OBhRq(7)T`W z6m-c;vqGK;S>3Lb6UWRm%AqRcgj#V5K?l4(UkRR#=~Yy63hHehq;FDhP6~+-1PVZu zqkrwuCJT>#IvU&f>O2UFU=SG|noJl&^zHp*o&6*~J*WW_0AoF5>#luUO*G&Nl@a>w zSwf6Uz=u*73NS8lkvsh(2f9heCc7s{_Yk4B0p|{V=FG^;o}v<~MA(u6P68af)Mij; z>5ng$=?HR2TYFm;&a!qv!;*rPr;tX2BIY^vNj~=3cYcPlolaJn6qBNZ2k?McnR=Vv zfP%us*>VL`eQ3uiQ)K`}Rz?Y~q`R;RmhiMOZm6gjN+4W@%bvKG6^I~%f-?*`46Q&2 z0ORq>2|E`J+gsu>1_z$i3Lvk_!8T5GI6Izk_E(s2-G<&GFhOZL4ZUAeiJ-9w1`IGGUtYaSs5+ z%i+qct3+`jnut&ame@o9RuhNH8kiQr@-dNldxW$n_^|-PrArQL-~`|^_dA$ zBZW4rG@x@;CQG(He_IO8izsQ>I?av@Qi^p9i_;LoB*HhLoy`idgK=wIX}jXDR5=+} z1A{138XWnABgah$0drB>QMte-u+mb>;kIOqmD)Z5Dy_&TRWWIp!5C2t+km`-jDP_Y z>hny5!-0grVg*i&NCo4O_t=bablw-?ifjpxw8^S=4AXWY&q#{Z!MTu=I3h&C6Mzj1 z$4QNG^Muhy#q;a;H0`=L&%9P06Bt<{9C-#DZK5cguQSiOonPUZDb6?Hkc#4z@hOUQLi@5sAU!#e<5F~0W+)t*b=BqobWsWdGR1gz7G?OKJwKm7a6;GVL?i+$zB@O{QK3~|RI*)ZfhAh<`tf-<2pejfMnAuJ+ z6aliWPNJoXJz8o!u_jO9s^LT}kb%k|Qc$oVF_Ss=oD_~LWz*5N@%1dL20(;a*q)F* zA?GQ;_lrp&D+-3f`3le5F>Pn|jV8~4sch*CeT<-)V?qMP40-03E6^#`;R*rfO(uo2 z>KW2rhd)$7yA&Nxz|jiXa%)W;X(w2O#`CIE&bmz_{~co*RAC@fYNzJ1Zs^qN|J=(s zjZ;uWN(5FLaxjJiATWq7^rvC7LOr8}ibbqV(3T&+_C~jR@3{qse4k02R)XeDy>8`v zlrPBEP%5ZaRV5poH50~ zSP81ndX3(S2>JY&Iu10YY?vz5;)MV(H3jAjFlPzT$-_WY=uBy>P=T4_?;lMu1*3}& zRRVdr53*c3Qw?)kBNPg%v!m&$Y3DeO)`hV`COI%=ri%5!5?MqdtXJt|#>rq0Q#&af zz7$MlM`nag9uQwWz|zTnskS{B43QAVEZfy28RM43+ zbG~;Ox07~gFea!dGVa0>2!I*dW>{O_hUT=iQz@QJ_@yZoOi0vJr=@2gyViBMxHiEM zbXJ%uS_1!v%%Iv~Z9prP1clMisOg<4L?6+k=;mQSwKjZ-B*p@XFlpM}_ulTEK1RNA z%poa9tzzmw(jn%7uHUb$I}BKEzpsD=3PT|Gu0a9g`6cl@&dJ)Ijs#GiL zdQ2Psn!N0i<9*ygM)DlH8BYTC_I(2gT?b=#6(EiPaI3h8vSFbK$n@Q@z`r7b9v^2Y zB%@88cme~f>CwDM&_9=Q?UHj@LW)8DS}%b}(wvHKAS8Jyld5W%iNmHZ4Qa~6z`4ck zKU$A_sO33LK#mkfOB^b+alGLavoQ{RN|XRtqrJpH2tE)%1xX@dlw)u<-4Y_8K(e|_ z0$3m;&)Ch}lWYoLl&^YL%mAjNvORBrB7tEx!$d;PKi?zb0Hzc{bE!P%%i7U3)Q}LgBZL*kr^_$0lyr*n_QJ9Ks-26uL@IO1wd$)DU=UkQt>u-Cqc`ru}t>0 zfpW*ch|^AR(c==`3ts1T|XK97v?<0 zsA^Ow#}D4G!F50$Ba?P`jK(a$Q*MO{BtX}DpCm!Gr5z!b0YreqfrNq{fR6)<73>a0 z*g&TqqpZp^uV-LFj{q4M*&D!U*h`l$hure0KNNy)0M0u`GDj<#BMr!#6djEO23Ipv zc#Kp-^TaWdr*s1#^4CNrWnyuGCG-@+xqG}1Qd9tMx0k3$!&V1Ci0~@lQ|4Q&0pz0{ zk}iqB1RP;yRwwtm7e_w`Iavs#wG-!_tCiAaPMlZH6#`%BsPJvNG#rzg)1!~-F8}8% zPItf5iayUOIGd9(+-R!#F@8O;K_SujLJz|>p! zxJ}d50&xjLk+=-q$I(mgvvMiPeFCwEc))V0D~9Zgt2VkK0Vu-ZN(7cOTo@u$h_*tP zrbqQdMV-!?$GC0gvK$h~oQY>%d3t*1JKKBQnY&IejPc|rYMxzlhQg|#UY&t#cox!fj_gla(aPluQi9D67xyoH2Lw7}MFlqq5f^ znoHr$$V&ZOLN9M+&0h?`q;q=OooBi4TIt^Fr3Y-b?5txYIEdzJkgkc*6HK=L5qG}v z*T3>#yRGmazWwzI6x1=x;~E4Ph&T-icB7)P{@_$P#GFwocc=&?S7T7~ukJ zUbu|joP8^X;K}J4K%!fL?ikIm+HxPPix{0DP87l8*z75@Wl{&E0IfH4lp8v zbW}EI|EG3?r{rk6c=~QoW zMX4-MO;Ci~xXM)7ZM6+IEViCDfUETry^+BDg5?=7l=^;x{O$$D4Q9d|iDRn}rYk@K z9Q9=F(n3(4epvK5JsOJ-B-hl3rJK6(V$cO!3;*eOnI@6vlp<3N=P3l!vhrg@6U~kc zzNFKpdoLh*hH_NvH;ycOlL0zo-b@?l+?};y4S=teMZ&ae2cS7R7mIt46?ug2GeYKa zG2r^*YS4vH9nIsY(;Lu`JiR_$F3sNfH4h&X53wlKZ_qFaxqx1ZNRtF@a5Ljf1h6LA zNb=e_d@6WDZ>kz4(*W4JfJ-LgXViLt38;kJOZ?eu2NR#LpyU=33^3C%V$}9k38Z$TwB%2Tvq;=0Z;zliGBvmTvDuAtH}#lrR*C zAYC%{rfQpjeF4|w3KEE?$(29|>YofqmycJ#IRB9;65Z41f{|ix6l?SOxWI9hfag~b z7PGF4wW!cK>%I72HCsdOl*KYTbpY4yZ3ES$K&>}Z%K$NBy%ol;O@=woK%O^KqX=yD zOdR~vlPUU}*ZzfiHMR2}nVOn*aH9U5+KYNJfHAWIfX=kDRCncqp>rGHHkLnrZfx<| ztm9U`0+xU}8{BsG1!JmSoh^fn_2e{R$jqqz`qLG7Kdh3rhU8bO4N!Y(cB7oqI+$uH zRsu&$$jwsj|4)mQVV*`sG0bxSHOf8Z=S`8|&@~-5Oi)u#01}w#(vnPjPo@BQ2J}qW zQ4RZ1N~SoFz78Cva<>p;RM4sR$6fVh%`lyQ#Mk+YF#6OR=!K`mqH2P*k~7nq6!yC%b|rT*2%LrT_{?cTy?^0%`ftENwdV7~=Qhr;Z5?eOAuV)3S-~ zj0+0S3N%lFtB!!?QNg(Pbu_e9MO6Yr>Ac^KJ45dFw{w-jNL`eA1cc^BrB!58rGCza zafRnsHR{io^=Q{&+@|9;zE)K}6QS+CT3SO%PNiS=EXEbdR|>>+7(D@NzJjn}hy&Q^1VwdyOW2cGFRl z_J)z+_#N2FTQC$(opRu~nGxB*0PH2+P(S`@r!F*gA$Pl5V(R^mG~Dzeu!Mz)2Zvv< zvq})s1I!sXYg*eYrmSzz+LuhvXpCgJ0c^*2x z3Klq|;g%|44aj5v3hYyzHqmjz!i)89!IbLBC;&Z1-&1KQ)8wv)V|J1QrXnfYTCcGKu{}T2A5R@)_(R1@JZnWOFwnrJmyJT z#SBGd&BuO4po2(96acLOUCVC;Ohs2TO%McBk0{X_&efRfj97)5i_o)(;LGYnDw~5o$?W5Gn)Y=Xv2OPVBw0!#Gl-tuF-&|kTW3v zoP*f+4x?1lkN54q-PIy53N5geqfe{(h{b>@%$!rdd>wl3C$vvT2+5-<+jBsKxNszn zpLI6PFs*OJ`er@iketf1Hawn8-x+PF!5Pq04tCdFS3pvMc-N>_D(9hAvQ+4LpbM~= z&~#1X-OF)162hE;GmIHqqPG>3R|7T1)Z5i!lCUn0irV;UBnL|l>-wumN3PSLc8qqt z3=MQ#tWS<&>k*iG%I*Y}!fEp1?z^Vdg#EVFHpM7eDn8zHDPy{VP^g_&Y+C0L=VT_T zfPrEsH9fU|W@Db-1q070Q`pr!`7pl%i6dmV0rvA}mdW2{AmJ78S3a77@JiVXnAf1p zh)80IM=n078w@Hn;JLL@rJ}vjyaGf*CaNOG#{8i@`_-v6B~O;jlnHzTuvsTdJt@y@ z!}>jd>by5FnEI3kjh~(|R1LsTOEKr~Gs>ZpbHVznk(`t`btKraGq;)!hTgW7c5%>3 zh0;wx9|j1xWD4P%58%U(%_9{GF(lLW^thq-0t1F!jD;t?YyxyCn6V9#_385?%ArPc z{A4_$j#}xw3XJ2ddys2HMP=<$H!jUl3Qey!IaISsDpYyo00e}19hD=(sosr!GjiNjND% z*R5&sH=Ue!6zf9oWBJ?7;_WNFK4_i~>xf01D~f!;&9bp4RL>Yal?I%)$7gL%{iblC z&6t|0HL|{SYE8{hC;+UTqE=(rc#7V51726hAlcIjjPU!%l@1h84Asfd0Zqdd(<%j` zDBQRQ8&u>B*NPpB3s`@exAu4?CsRial5w$n>TI^5z0xM(N?hfF>NBe5gY&4KY7JL#Ybm%YINHEIel8D0qN z5w2!+oRgu80_+V?V9a(sYa&^{_Qx4AwMpPK=KKio{E8t9lv3KnsV~G2+r|8b^7a)= z!lsy(R3{?=&e2JZmd<;hb=&}+?qO6jkWlV0nF=A4 zeQg6YsU;J)dsd5@frKQb~^yR^{GY9p#93pz^m8BCS4s+poxnq4V} ztsTEvZczUed!jT>`-zbP3^2R-h22R13_OR!bJJzx_l!3ndk;4D#Pf>!aZR&rXVicz zwFW2$TZI{is&aVDt>%|>blpk@vvLMwo7rI8;L;iwQ=IZP))^PaMy2UCOvmAV07waa z!Y+Y!p$;UuE+;^(bzb2=KN!^le6+vW&bi?5vPJP8x=Bq0}E@Ka7_%iCS@Z1*GCdmQ2 z;3eQ|T}oK$f$^qgFx48Mp!j$pK@EuRV_Ng379Zp_s$Op~c`=q{)B@j11U|>jwDN z=VrB>9R(Y=`SWpcFcyT$7u}9^2FjWG1Q7BP9$+YGQ0mhQ+o*KP2~7EJDyXkDo=_t# zxYSJa6}97Qh22oN12+K1Ol5-eBqdeF(u4MQiw!7h0l34B_0$b+B`}2HQu4&l6Ikk1_9##)CpEftIu7@H;`JE_L$DuP!xUY0bVBgcyUCrN(I*eIT}^qk z^E#QLS^%V13j;xqL15U_gS}N_MNnl(7GOs`#(8LtsIoU$nH0GJd*agX2&4cGPxq*; z3Rjp+Wq5l+Vn{gP6XVijh3LTW3z*u)H0a>@_4yUXZB*NU8$j3aM51hpEtBV0dYad= zclLkZ{zfbKlK(JHzV~w59J!q4H7N6yN?S3wf6}m@--H!9xesuAhbcSa>#YaQS3>ZK z+L-nQD0(g!-Zhk?jpK%IlWE4k!Nv8of})Q5M!EYaA%p}k{e;rPJP?Sd5Svy)T(3gB z(4kO-HT@p*-Z(}<@xnWv2l((2;NE&g+8gl(DDq9>xQ0OQ7$_LEH)&}X0LNjZdRjU# zErbl{q|KXRYyiaoJWWT3?!ULI`#Apm?}xSo-|@Zg?$`b&K3M>$ieSz#EJjbF!%}cu z@por61p^c%kT$M{DV!VY0Om;i?2>I!kdp^+#x~BFt+6TwIKazmfB-e7n3_`AB?9cZ z;_{BNU<%35O#=tTRKPIRkN~7@T!}wr7x5K9?K?mZxdAY4<3`z~>av290GH&Z(}hD8 zEKSNe*J0W())Mft|DvDg*H@<6;9NHaUrRt%r^5oYL-Nf)03J7cCVHJ{^HnVkidmo1 zu4p<|7?LNX(Kptg`uWbVx2ccuB~3fRNyY|YK-c$M-#cJ9z#wsKhB*ucQ)*<0%hP?= zwB6~%wFi*sVuX_^u7D|kail!C2AF`JjFCgXg9~2TQtHz40#uC~9kfddI)kO<;v`Rr zJ6+tl+PKs8;}^7sFMIVo`}SA$g@J@dpVxsy!;LyXhZQKqV+U|5Rd?HT{;ucu03YLt zxc+9P+WoAihzG|h-o0oKierC?=_A2UuNLz*XJB$Nl@fpuO!U+WL8VP-PqhFDrj!*V z3`UZBO7>1Or>~G)yD?x zxOtCFFuiscMJ0qv)93;dtV|A!mvQ zm3SH*ujXnzZPtLQGcyo^kyKx7W^e3?LJXAkrv~re0=%f^8MOj<^g?n7VocM6CkJFm zmT)G}cQ0!C?!^@$03?pFd=Ko4W+Tj<^XPnA%%~jY>5n04CG^MNW=BHa&n40F$W$j^Wi z|2fkZOa!ZNMKdzBbLNio_(zT@FqtR%$yu$+-0uX}9RG(3G z$iO^YhKjE<>dmwJ^p3LpXg06rPVL915lBM<^nBf1r{_PxXmNz(`tt6@KmptIDt2Cs zXGM+euEve}Dj@O+DKgW}-TI2-yR?%3nX~^${5^bp{LUHwNpe~~HaAuVkdbFnK$b7G zSAYu844^2$aqY+TtZMqxX+5LB2W$#636I7d8YhKI(*-j1&G+#X@eFQ*G3PdEI`dOq zLCw_WjbsX@fF;z1E2gGCEmATCNJ^!N3LIC;mV|K7GywVxE7vooD9{GH1Dgy*4dV_K z1JIO9zZGg-chpt!++(B7{J0nX;n$Xezt8se{5L=6w)$87Xn&x;0rC^hPo;ORLKmqG5Im9|UiTJd(zfspuME9YIS4bUZioUfH|1X)SkO68d|< zvpDP zBfvL2hN;fk*1Sc_apHm6ghM0i!y0@h@Frq6TCYenoqYefpX|r%(e8>cx5=He zWrM5p%&I-rlQ|L<9|JIx;Qm+b=i{IJv+sDZi|gAwPyN(Gvv%#rp>3A#xVHNK?Fuc; zPy2pSWy7#pd(+QnEgHPTzIpdd7uau?1u=lZ6B<`G%~s7H*W~G)w4DFo&d0yuS^vg5 zK8XOM;wp#xJ$ijrKzTTJ3NKfmEF_!$tTz-;_$jH{MpaFGp%h_tk)NM6tHF) zw|TP-;Nx+S@CV@1{_qC@y9KvoJ*3^JSi_INxKV8ntj8`9gjo{ndmIN$La$(&_AZ|P zs7C^uqGw&V03up`;cVg&8)BN=VQabNqj|tTd_fEP_x$lEA7`uM(9|Pc2)CX3lHt>pnsx!$joOVC*iGdlyyw8=3EYpXb7*5KZ-U(?e)aUR z0S8d)!Owr_$5?-App;VtuH)hru-lY6E*Zd3fOBr) zAv%BnEcMto!`mwmyV1n(X${IJb|Hdq@u2MsHZH0^uKMH=TGGFTH;2E$nYZEQ{`-jB zCGM(+>j;MtIQ{n0*>@eb^8J>!@wgH=ElWB-CLneTv1r?4$kH}FU?H(C5dA5F7znrw z6cu>}hZZ0>j^K%AIFf%@;`>tXL5O>u46Spa57(-qb=G}d)p)+Kv zk)V1_3QuF{fBvtsUXC+hdW*t8;McYJcO3Ws&u=0I#Y$9(wqYq631u^`8K65Ul?Dby z`UAQCR-W30@}KLeybP@c$}J7FYLMt612hg09Ze@;3P_hHvtWoVMK&9{W%W4{K??EY z1P@e7NCS=FrNhnOiXqEc?TV~MS(QdSR)AO)Fn}|>H_$(h6IqR*{uTIT=7Z6kvwE@P z<2>^}-l1jvub;Vv3b)Vq4>^{&Lb_h_MB=`Pf`OTCO06knGej1RsaH^p7e~QxJ_ogG zHP5e9=SvV&I_BIux~+|_`=zX_ABP7er(+Y`Wkg6^2JXIfmA`NWJe?~*)f9}fQGEv9 zf9j4uK@Edf4AfB72_UOw=^m>WJ6?=`f0P#XcaEP5f3xkHuPwc2wZD&?#%@}+8_rDh zR6VnCwUU|W1^?~Wpic9o6F3SA23BR5-sYCZEzPdP$4toZ;J^BfE36^%QSSr@2ENEM zs8X&_gDaH=U|L}Yqdk7B&#IhZ%YdQEL|NxHR>_Z~{@+8ivj4r`cApUR=^UK|KtA;KJMIoY`}6_+0W*M>6wFjXIowp)8^E|RZlkK%rgLV7#ecIQ(L#)} z&5eAgrhwTtvOX=qOr9antcnB(c)J^+b17e*2+A{7)OqH_P zm2&FMP=E|-YAUM&XEiG20viSw>TiyY{)-=f_OpX;T3`c0K|?z$fD9rcBBqMw0Er^P zDrUAFu(^R!t}y^RQ*U4uR;yrVGwcjjaiD6+o?^wyfEijfXUvdAG43=-10=laY}6}| zU{5@6n3dqnmGkv^aZp_d3jh_n9c_(6lGfNf=omWQWz&|{w*)EUdI>}{Wba&-Tk}8RoVWf zYsfNytQjOa1uVcelrdYz49pC(7Y^Pqz|IQenijW;YBkPOn;FJ7TMddD7?lBLMp;qJ zs5CRfIO{Q&tP(q$nK3vO1}K18VlXrFEgNP={dX%`i~R%=|F55@r|Ap4rlH2&kDFM%e%btfJOR6FxbI zDpoQxWHlsjl+S>fno+h23nMTXV^09>R)D8<8Ze9l(|0_v;$Q_0Qm~u8%)FSaT|kv` zugNK!mingi>%9*`YlE>6$Rz$?!$oOkriK~iCaqg?s^<#B?pHu2w7Hl`7!hh}mn2x2 zTLOm+a7}Jzss>z3suS*xL-JBX#}zO&o_X&;knl8_yjpT5kkW*i8N!NSW(H=^tXo_V za`KrB&G+1G1nl2Ge$=-#UfbTxxkB^D{eV}6wAkiEQFEKAuRgCWYG;NdVI{g~%0~TW zKB9!-GY4+Sr1_0V!_b=p2ngXgKqMHP>WjJ=Oud4_oZ+&W1L;T%H0ezM)x@CiY{>_J zLQRapzQEQstqG@W4$Ut)P=d0*hYnP!44LTfB!?1|c96-oap7*$)2Ig>JS1qjKa&$S z`!)BGWz<^@x2WDV$?^+xXbeYpFy+?n8Ni&{rjPZf4FhCxfA4TrXeEgF!4San?^BDfOJ-*3hn6nU@;2i)wvfS$Z?L@djo7Km8$>>@fcP>>K|`_d`K9U7>k@-$iowlXJ_Ar+5Rx z76!^0(1MztV8cL1Abz$rq}G5W3xe9z^E$4>)j%OltTyUqpa5Ze7VdZil^KZ7xqF{t z)djOLtFbPu{_})kfwg!|f%?*zH~}uF2aFf{OBZC{c!A0hH$DH|-L&Wi#2-M+xr@4O z+uNCqlZ_*~%r%;Jah~0jsFR0^pRD3SvM$OBu_t;k zP_w6+>M_8;0w85Hc+{n{v)+R!*PWwb>OV#6{?hC=-(n8GKhL)L&qkxP zj%K)Fbd8w>?O;f*unaVN5W%>?3JgxtEA{6m9|%A(fMM!5JOVs0OkgavVc{J0SvT(w z({6V5{~x5K{{_tjX!CAO&L4Y<^qA<>^kakvNNybf?(NO@mSzQNX|1qaqJ)CiT8 zAtV6K9k?0gdo|-qr820pbz$qCVHL+Pqq{h8FF_2pM7%gcfT`vLodYcErWVdB9(FDn z-QeH>VEy^rDQ9Rpj%x$&xGM^Roe^Tr4#ALhHAt z{_ZDV!qZQ-eRt+@bN6hF(lm$YQKxm8pf~kt-Gcg9cMKa!Jz!KW9d2N#ni|~i3B#QL z+JX8gz?Itb6&on8@J4Mbk-DEy(t9u zD$cux!#XqFLthO?`ao_uHV z%&3&%K2|@YfSm?ed%!G! zJrT>AYv8->c|pzSf+qu`0ILm-Z(@4yXQfpXvuopldtl3-puyjO?4H!8E`Y_6)j+pl zNv0*)6IxJQ#_Q&tC>!3LIW z2K)$|DSJtXw7}1@fxLb_ob^7|b?|TpVtsymg*52PF^2gkKtupYwmcZ+@Q=$90UpUM z!b#&sr8nDfAIpFm1J%iFnU%_|`%4B+f^p#i$J+RLN1HWwI$*;-_KTk^G@y)&qyPU4 zxWDv?x7l@Pb;qm!A`j`m?zt+w+H(B-4RV8ZQ0_l3Kdx(NM1T#bRW-lV`{i+On0Ipq zpV-!{PDHX5J1gElf%I`(`KhINYpBwWHkDT1b8PpB7Q7-Um}A=@yV zO-pS1v^P)~hNrycelSbEnFEo1Pk6g9xLRj`p~OuXt_5XTvbNkD9qrm&y6b-AoqwWS zev@5?`M16OcE*p&#n-Rl$)Eqm@u!@_b6?<9pSv>X^9g5Ohyfes7&-ksxVEmz$Sq5M zs@la0G_b%TjRPaI*BSyzAu1M_XqUx9IvtKtu;>VL08MGYFXKSqkdB3tVRa}N7ko%+ z$=(39pM|`4OsO=Ze7yl(Fi=jDDn8g09jIb%zKI*`30+wPYoh>;!VPiV>pg%++~}eEU>y)m0u0g-2xN39LSjg24vQ#xULjdKFBx7@ zgTWi4Yxcx*bX*H?G5byd64+CKDrGZ?t7+pz0_#b?5Zl0>xpE#ntsuG{FGgmt=yYrp z2H?kpLDLT;u@_uPfp|!yLbpz!bkhJjJQ|b^A?n@v`aWJ?>sFL@R@A)0|2|%|u@?24 gk7M`C{>78EzaW?R{}TUS;{Qwhe~JGu@&DVjC|yo*kN^Mx diff --git a/FloconAndroid/sample-android-only/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/FloconAndroid/sample-android-only/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 3d45e88bc8a2ed755d6f0f68c7c811442a6173a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31744 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 n?S>g - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/res/values/strings.xml b/FloconAndroid/sample-android-only/src/main/res/values/strings.xml deleted file mode 100644 index 7e4319641..000000000 --- a/FloconAndroid/sample-android-only/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Flocon Sample - \ No newline at end of file diff --git a/FloconAndroid/sample-android-only/src/main/res/values/themes.xml b/FloconAndroid/sample-android-only/src/main/res/values/themes.xml deleted file mode 100644 index e48770ab8..000000000 --- a/FloconAndroid/sample-android-only/src/main/res/values/themes.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - -