From 972b033be1abe9bb09f86633e03827968ab07e28 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 15 Dec 2025 08:39:25 +0100 Subject: [PATCH] v0.3.2-SNAPSHOT: added android-specific code and implementation, including defaultNamedStorage() --- .gitignore | 1 + build.gradle.kts | 35 +++++++++- settings.gradle.kts | 18 +++++ .../KVStorage.android.kt | 65 +++++++++++++++++++ .../sergeych/synctools/ProtectedOp.android.kt | 17 +++++ .../sergeych/synctools/WaitHandle.android.kt | 22 +++++++ .../kotlin/net.sergeych.bintools/KVStorage.kt | 14 +++- 7 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 src/androidMain/kotlin/net.sergeych.bintools/KVStorage.android.kt create mode 100644 src/androidMain/kotlin/net/sergeych/synctools/ProtectedOp.android.kt create mode 100644 src/androidMain/kotlin/net/sergeych/synctools/WaitHandle.android.kt diff --git a/.gitignore b/.gitignore index ca0502d..d9f34c3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ .kotlin /.gigaide/gigaide.properties /java_pid40366.hprof +/local.properties diff --git a/build.gradle.kts b/build.gradle.kts index 72abd59..81463fa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,13 +4,15 @@ plugins { kotlin("multiplatform") version "2.2.21" kotlin("plugin.serialization") version "2.2.21" id("org.jetbrains.dokka") version "1.9.20" + id("com.android.library") version "8.7.2" `maven-publish` } group = "net.sergeych" -version = "0.3.0" +version = "0.3.2-SNAPSHOT" repositories { + google() mavenCentral() mavenLocal() maven("https://maven.universablockchain.com/") @@ -19,6 +21,10 @@ repositories { kotlin { jvmToolchain(17) jvm() + androidTarget() { + // Ensure Android variant is published to Maven so consumers get androidMain APIs + publishLibraryVariants("release") + } js { browser() nodejs() @@ -57,7 +63,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") // this is actually a bug: we need only the core, but bare core causes strange errors implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") - api("net.sergeych:mp_stools:[1.5.2,)") + api("net.sergeych:mp_stools:[1.6.3,)") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") } } @@ -82,6 +88,11 @@ kotlin { } val jvmMain by getting val jvmTest by getting + val androidMain by getting { + dependencies { + // Android base64 and preferences are in the SDK; no extra deps required + } + } val jsMain by getting { dependencies { } @@ -119,6 +130,26 @@ publishing { } } +android { + namespace = "net.sergeych.bintools" + compileSdk = 34 + defaultConfig { + minSdk = 21 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + // Publish only release variant of the Android target so consumers (Android apps) + // resolve the platform artifact instead of common metadata, which is required + // to access Android-only APIs like net.sergeych.bintools.AndroidKV + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + tasks.dokkaHtml.configure { outputDirectory.set(buildDir.resolve("dokka")) dokkaSourceSets { diff --git a/settings.gradle.kts b/settings.gradle.kts index 6cf7e03..4905b8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,21 @@ +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + mavenLocal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + mavenLocal() + maven("https://maven.universablockchain.com/") + } +} + rootProject.name = "mp_bintools" diff --git a/src/androidMain/kotlin/net.sergeych.bintools/KVStorage.android.kt b/src/androidMain/kotlin/net.sergeych.bintools/KVStorage.android.kt new file mode 100644 index 0000000..70de165 --- /dev/null +++ b/src/androidMain/kotlin/net.sergeych.bintools/KVStorage.android.kt @@ -0,0 +1,65 @@ +package net.sergeych.bintools + +import android.content.Context +import android.content.SharedPreferences +import net.sergeych.bintools.AndroidKV.init +import net.sergeych.mp_tools.decodeBase64Compact +import net.sergeych.mp_tools.encodeToBase64Compact + +/** + * Simple holder to provide Application [Context] for this library. + * + * Call [init] from your app (e.g., in `Application.onCreate`) before using + * [defaultNamedStorage]. + */ +object AndroidKV { + @Volatile + private var appContext: Context? = null + + fun init(context: Context) { + appContext = context.applicationContext + } + + internal fun requireContext(): Context = + appContext ?: error("AndroidKV is not initialized. Call AndroidKV.init(applicationContext) first.") +} + +/** + * Implementation of [KVStorage] backed by [SharedPreferences]. + * Values are stored as base64-encoded strings to fit preferences capabilities. + */ +class SharedPreferencesKVStorage( + private val prefs: SharedPreferences, + keyPrefix: String = "" +) : KVStorage { + private val prefix = if (keyPrefix.isEmpty()) "" else "$keyPrefix:" + + private fun k(key: String) = "$prefix$key" + + override fun get(key: String): ByteArray? = + prefs.getString(k(key), null)?.decodeBase64Compact() + + override fun set(key: String, value: ByteArray?) { + val e = prefs.edit() + val kk = k(key) + if (value == null) e.remove(kk) + else e.putString(kk, value.encodeToBase64Compact()) + e.apply() + } + + override val keys: Set + get() { + val allKeys = prefs.all.keys + if (prefix.isEmpty()) return allKeys + return allKeys.filter { it.startsWith(prefix) } + .map { it.removePrefix(prefix) } + .toSet() + } +} + +actual fun defaultNamedStorage(name: String): KVStorage { + val ctx = AndroidKV.requireContext() + val prefs = ctx.getSharedPreferences(name, Context.MODE_PRIVATE) + // We use separate preferences file per name, so prefix can be empty + return SharedPreferencesKVStorage(prefs) +} diff --git a/src/androidMain/kotlin/net/sergeych/synctools/ProtectedOp.android.kt b/src/androidMain/kotlin/net/sergeych/synctools/ProtectedOp.android.kt new file mode 100644 index 0000000..e4cabf5 --- /dev/null +++ b/src/androidMain/kotlin/net/sergeych/synctools/ProtectedOp.android.kt @@ -0,0 +1,17 @@ +package net.sergeych.synctools + +import java.util.concurrent.locks.ReentrantLock + +/** + * Android actual implementation mirrors JVM using [ReentrantLock]. + */ +actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation { + private val access = ReentrantLock() + override fun lock() { + access.lock() + } + + override fun unlock() { + access.unlock() + } +} diff --git a/src/androidMain/kotlin/net/sergeych/synctools/WaitHandle.android.kt b/src/androidMain/kotlin/net/sergeych/synctools/WaitHandle.android.kt new file mode 100644 index 0000000..f5c1dbb --- /dev/null +++ b/src/androidMain/kotlin/net/sergeych/synctools/WaitHandle.android.kt @@ -0,0 +1,22 @@ +package net.sergeych.synctools + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual class WaitHandle { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + private val access = Object() + + actual fun await(milliseconds: Long): Boolean { + return synchronized(access) { + try { + access.wait(milliseconds) + true + } catch (_: InterruptedException) { + false + } + } + } + + actual fun wakeUp() { + synchronized(access) { access.notifyAll() } + } +} diff --git a/src/commonMain/kotlin/net.sergeych.bintools/KVStorage.kt b/src/commonMain/kotlin/net.sergeych.bintools/KVStorage.kt index 0dcf93f..b2a8098 100644 --- a/src/commonMain/kotlin/net.sergeych.bintools/KVStorage.kt +++ b/src/commonMain/kotlin/net.sergeych.bintools/KVStorage.kt @@ -218,7 +218,19 @@ class MemoryKVStorage(copyFrom: KVStorage? = null) : KVStorage { * - otherwise, the folder will be created in "`~/.local_storage`" parent directory * (which also will be created if needed). * - * - For the native platorms it is not yet implemented (but will be soon). + * - In Android (new!) it is implemented using preferences, but initializing is needed in + * main activity `onCreate`, somewhat like: + * ```kotlin + * override fun onCreate(savedInstanceState: Bundle?) { + * super.onCreate(savedInstanceState) + * // set the Context which Preferences will be used for + * // defaultNamedStorage + * net.sergeych.bintools.AndroidKV.init(this) + * } + * ``` + * see also `AndroidKV` and `SharedPreferencesKVStorage` for Android + * + * - For the native platforms it is not yet implemented (but will be soon). * * See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like, * and `FileDataProvider` class on JVM target.