diff --git a/crosslens-core/api/android/crosslens-core.api b/crosslens-core/api/android/crosslens-core.api index ad279ea..05646e6 100644 --- a/crosslens-core/api/android/crosslens-core.api +++ b/crosslens-core/api/android/crosslens-core.api @@ -36,6 +36,46 @@ public final class dev/teogor/crosslens/core/NameFormat : java/lang/Enum { public static fun values ()[Ldev/teogor/crosslens/core/NameFormat; } +public final class dev/teogor/crosslens/core/concurrent/SynchronizedMap : java/util/Map, kotlin/jvm/internal/markers/KMutableMap { + public fun ()V + public fun clear ()V + public fun containsKey (Ljava/lang/Object;)Z + public fun containsValue (Ljava/lang/Object;)Z + public final fun copy ()Ldev/teogor/crosslens/core/concurrent/SynchronizedMap; + public final fun entrySet ()Ljava/util/Set; + public fun get (Ljava/lang/Object;)Ljava/lang/Object; + public fun getEntries ()Ljava/util/Set; + public fun getKeys ()Ljava/util/Set; + public fun getSize ()I + public fun getValues ()Ljava/util/Collection; + public fun isEmpty ()Z + public final fun keySet ()Ljava/util/Set; + public fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun putAll (Ljava/util/Map;)V + public fun remove (Ljava/lang/Object;)Ljava/lang/Object; + public final fun size ()I + public final fun values ()Ljava/util/Collection; +} + +public final class dev/teogor/crosslens/core/concurrent/SynchronizedSet : java/util/Set, kotlin/jvm/internal/markers/KMutableSet { + public fun ()V + public fun add (Ljava/lang/Object;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public fun contains (Ljava/lang/Object;)Z + public fun containsAll (Ljava/util/Collection;)Z + public final fun copy ()Ldev/teogor/crosslens/core/concurrent/SynchronizedSet; + public fun getSize ()I + public fun isEmpty ()Z + public fun iterator ()Ljava/util/Iterator; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun retainAll (Ljava/util/Collection;)Z + public final fun size ()I + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; +} + public final class dev/teogor/crosslens/core/startup/ActivityInitializer : androidx/startup/Initializer { public fun ()V public synthetic fun create (Landroid/content/Context;)Ljava/lang/Object; diff --git a/crosslens-core/api/jvm/crosslens-core.api b/crosslens-core/api/jvm/crosslens-core.api index 7602c25..60bed64 100644 --- a/crosslens-core/api/jvm/crosslens-core.api +++ b/crosslens-core/api/jvm/crosslens-core.api @@ -31,3 +31,43 @@ public final class dev/teogor/crosslens/core/NameFormat : java/lang/Enum { public static fun values ()[Ldev/teogor/crosslens/core/NameFormat; } +public final class dev/teogor/crosslens/core/concurrent/SynchronizedMap : java/util/Map, kotlin/jvm/internal/markers/KMutableMap { + public fun ()V + public fun clear ()V + public fun containsKey (Ljava/lang/Object;)Z + public fun containsValue (Ljava/lang/Object;)Z + public final fun copy ()Ldev/teogor/crosslens/core/concurrent/SynchronizedMap; + public final fun entrySet ()Ljava/util/Set; + public fun get (Ljava/lang/Object;)Ljava/lang/Object; + public fun getEntries ()Ljava/util/Set; + public fun getKeys ()Ljava/util/Set; + public fun getSize ()I + public fun getValues ()Ljava/util/Collection; + public fun isEmpty ()Z + public final fun keySet ()Ljava/util/Set; + public fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun putAll (Ljava/util/Map;)V + public fun remove (Ljava/lang/Object;)Ljava/lang/Object; + public final fun size ()I + public final fun values ()Ljava/util/Collection; +} + +public final class dev/teogor/crosslens/core/concurrent/SynchronizedSet : java/util/Set, kotlin/jvm/internal/markers/KMutableSet { + public fun ()V + public fun add (Ljava/lang/Object;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public fun contains (Ljava/lang/Object;)Z + public fun containsAll (Ljava/util/Collection;)Z + public final fun copy ()Ldev/teogor/crosslens/core/concurrent/SynchronizedSet; + public fun getSize ()I + public fun isEmpty ()Z + public fun iterator ()Ljava/util/Iterator; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun retainAll (Ljava/util/Collection;)Z + public final fun size ()I + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; +} + diff --git a/crosslens-core/build.gradle.kts b/crosslens-core/build.gradle.kts index 5651110..a4ba03d 100644 --- a/crosslens-core/build.gradle.kts +++ b/crosslens-core/build.gradle.kts @@ -86,6 +86,9 @@ kotlin { implementation(libs.androidx.startup.runtime) } commonMain.dependencies { + implementation(libs.kotlinx.collections.immutable) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.atomicfu) } commonTest.dependencies { implementation(libs.jetbrains.kotlin.test) diff --git a/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedMap.kt b/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedMap.kt new file mode 100644 index 0000000..578974f --- /dev/null +++ b/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedMap.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.teogor.crosslens.core.concurrent + +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.internal.SynchronizedObject +import kotlinx.coroutines.internal.synchronized + +/** + * A thread-safe implementation of a mutable map. This class provides synchronization + * to ensure that all operations on the map are thread-safe. + * + * This class uses an internal lock (`SynchronizedObject`) to synchronize access to the + * underlying mutable map (`delegate`). All methods are synchronized to prevent concurrent + * modification issues. + * + * @param K The type of keys maintained by the map. + * @param V The type of values associated with the keys. + */ +@OptIn(InternalCoroutinesApi::class) +public class SynchronizedMap : MutableMap { + private val delegate = mutableMapOf() + private val lock: SynchronizedObject = SynchronizedObject() + + /** + * Returns the number of key-value pairs in the map. + * This operation is synchronized. + */ + override val size: Int + get() = synchronized(lock) { delegate.size } + + /** + * Returns `true` if the map is empty. + * This operation is synchronized. + */ + override fun isEmpty(): Boolean = + synchronized(lock) { delegate.isEmpty() } + + /** + * Returns `true` if the map contains the specified key. + * This operation is synchronized. + * + * @param key The key whose presence in the map is to be tested. + */ + override fun containsKey(key: K): Boolean = + synchronized(lock) { delegate.containsKey(key) } + + /** + * Returns `true` if the map contains the specified value. + * This operation is synchronized. + * + * @param value The value whose presence in the map is to be tested. + */ + override fun containsValue(value: V): Boolean = + synchronized(lock) { delegate.containsValue(value) } + + /** + * Returns the value associated with the specified key, or `null` if the map contains no mapping for the key. + * This operation is synchronized. + * + * @param key The key whose associated value is to be returned. + */ + override fun get(key: K): V? = + synchronized(lock) { delegate[key] } + + /** + * Returns a set of key-value pairs contained in the map. + * This operation is synchronized. + */ + override val entries: MutableSet> + get() = synchronized(lock) { delegate.entries } + + /** + * Returns a set of keys contained in the map. + * This operation is synchronized. + */ + override val keys: MutableSet + get() = synchronized(lock) { delegate.keys } + + /** + * Returns a collection of values contained in the map. + * This operation is synchronized. + */ + override val values: MutableCollection + get() = synchronized(lock) { delegate.values } + + /** + * Removes all mappings from the map. + * This operation is synchronized. + */ + override fun clear(): Unit = + synchronized(lock) { delegate.clear() } + + /** + * Associates the specified value with the specified key in the map. + * This operation is synchronized. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * @return The previous value associated with the key, or `null` if there was no mapping for the key. + */ + override fun put(key: K, value: V): V? = + synchronized(lock) { delegate.put(key, value) } + + /** + * Copies all of the mappings from the specified map to this map. + * This operation is synchronized. + * + * @param from The map whose mappings are to be copied to this map. + */ + override fun putAll(from: Map): Unit = + synchronized(lock) { delegate.putAll(from) } + + /** + * Removes the mapping for the specified key from the map if present. + * This operation is synchronized. + * + * @param key The key whose mapping is to be removed from the map. + * @return The previous value associated with the key, or `null` if there was no mapping for the key. + */ + override fun remove(key: K): V? = + synchronized(lock) { delegate.remove(key) } + + /** + * Creates a shallow copy of the current `SynchronizedMap`. + * + * This method returns a new `SynchronizedMap` instance containing the same key-value pairs as the original map. + * The new map is initialized with a copy of the entries of the original map, but it operates independently. + * + * The copy operation is thread-safe and ensures that the new map will have the same entries as the original map + * at the time of copying, but subsequent modifications to the original map will not affect the copied map. + * + * @return A new `SynchronizedMap` instance containing the same key-value pairs as the original map. + */ + public fun copy(): SynchronizedMap { + val newMap = SynchronizedMap() + synchronized(lock) { + newMap.delegate.putAll(this.delegate) + } + return newMap + } +} diff --git a/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedSet.kt b/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedSet.kt new file mode 100644 index 0000000..6033333 --- /dev/null +++ b/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedSet.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.teogor.crosslens.core.concurrent + +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.internal.SynchronizedObject +import kotlinx.coroutines.internal.synchronized + +/** + * A thread-safe implementation of a mutable set. This class provides synchronization + * to ensure that all operations on the set are thread-safe. + * + * This class uses an internal lock (`SynchronizedObject`) to synchronize access to the + * underlying mutable set (`delegate`). All methods are synchronized to prevent concurrent + * modification issues. + * + * @param E The type of elements contained in the set. + */ +@OptIn(InternalCoroutinesApi::class) +public class SynchronizedSet : MutableSet { + private val delegate = mutableSetOf() + private val lock: SynchronizedObject = SynchronizedObject() + + /** + * Returns the number of elements in the set. + * This operation is synchronized. + */ + override val size: Int + get() = synchronized(lock) { delegate.size } + + /** + * Returns `true` if the set contains the specified element. + * This operation is synchronized. + * + * @param element The element whose presence in the set is to be tested. + */ + override fun contains(element: E): Boolean = + synchronized(lock) { delegate.contains(element) } + + /** + * Returns `true` if the set contains all elements in the specified collection. + * This operation is synchronized. + * + * @param elements The collection whose elements are to be checked for containment in the set. + */ + override fun containsAll(elements: Collection): Boolean = + synchronized(lock) { delegate.containsAll(elements) } + + /** + * Returns `true` if the set is empty. + * This operation is synchronized. + */ + override fun isEmpty(): Boolean = + synchronized(lock) { delegate.isEmpty() } + + /** + * Adds the specified element to the set if it is not already present. + * This operation is synchronized. + * + * @param element The element to be added to the set. + * @return `true` if the set did not already contain the specified element. + */ + override fun add(element: E): Boolean = + synchronized(lock) { delegate.add(element) } + + /** + * Adds all elements in the specified collection to the set. + * This operation is synchronized. + * + * @param elements The collection containing elements to be added to the set. + * @return `true` if the set was modified as a result of the call. + */ + override fun addAll(elements: Collection): Boolean = + synchronized(lock) { delegate.addAll(elements) } + + /** + * Removes all elements from the set. + * This operation is synchronized. + */ + override fun clear(): Unit = + synchronized(lock) { delegate.clear() } + + /** + * Returns an iterator over the elements in the set. + * This operation is synchronized. + */ + override fun iterator(): MutableIterator = + synchronized(lock) { delegate.toMutableSet().iterator() } + + /** + * Removes the specified element from the set if it is present. + * This operation is synchronized. + * + * @param element The element to be removed from the set. + * @return `true` if the set contained the specified element. + */ + override fun remove(element: E): Boolean = + synchronized(lock) { delegate.remove(element) } + + /** + * Removes all elements in the specified collection from the set. + * This operation is synchronized. + * + * @param elements The collection containing elements to be removed from the set. + * @return `true` if the set was modified as a result of the call. + */ + override fun removeAll(elements: Collection): Boolean = + synchronized(lock) { delegate.removeAll(elements.toSet()) } + + /** + * Retains only the elements in the set that are contained in the specified collection. + * This operation is synchronized. + * + * @param elements The collection containing elements to be retained in the set. + * @return `true` if the set was modified as a result of the call. + */ + override fun retainAll(elements: Collection): Boolean = + synchronized(lock) { delegate.retainAll(elements.toSet()) } + + /** + * Creates a shallow copy of the current `SynchronizedSet`. + * + * This method returns a new `SynchronizedSet` instance containing the same elements as the original set. + * The new set is initialized with a copy of the elements of the original set, but it operates independently. + * + * The copy operation is thread-safe and ensures that the new set will have the same elements as the original set + * at the time of copying, but subsequent modifications to the original set will not affect the copied set. + * + * @return A new `SynchronizedSet` instance containing the same elements as the original set. + */ + public fun copy(): SynchronizedSet { + val newSet = SynchronizedSet() + synchronized(lock) { + newSet.delegate.addAll(this.delegate) + } + return newSet + } +} diff --git a/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedMapTest.kt b/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedMapTest.kt new file mode 100644 index 0000000..36bc45b --- /dev/null +++ b/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedMapTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.teogor.crosslens.core.concurrent + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SynchronizedMapTest { + + @Test + fun testInitialSize() { + val map = SynchronizedMap() + assertEquals(0, map.size, "Initial size should be 0") + } + + @Test + fun testPutElement() { + val map = SynchronizedMap() + assertEquals(null, map.put("key", 1), "Put should return null if no previous value") + assertEquals(1, map.size, "Size should be 1 after adding an element") + assertEquals(1, map["key"], "Map should return the correct value for the key") + } + + @Test + fun testContainsKey() { + val map = SynchronizedMap() + map["key"] = 1 + assertTrue(map.containsKey("key"), "Map should contain the key") + assertFalse(map.containsKey("nonexistent"), "Map should not contain a non-existent key") + } + + @Test + fun testContainsValue() { + val map = SynchronizedMap() + map["key"] = 1 + assertTrue(map.containsValue(1), "Map should contain the value") + assertFalse(map.containsValue(2), "Map should not contain a non-existent value") + } + + @Test + fun testRemove() { + val map = SynchronizedMap() + map["key"] = 1 + assertEquals(1, map.remove("key"), "Remove should return the removed value") + assertFalse(map.containsKey("key"), "Map should not contain the removed key") + } + + @Test + fun testClear() { + val map = SynchronizedMap() + map["key"] = 1 + map.clear() + assertEquals(0, map.size, "Size should be 0 after clearing the map") + assertFalse(map.containsKey("key"), "Map should not contain any keys after clearing") + } + + @Test + fun testPutAll() { + val map = SynchronizedMap() + val entries = mapOf("a" to 1, "b" to 2, "c" to 3) + map.putAll(entries) + assertTrue( + map.entries.containsAll(entries.entries), + "Map should contain all entries after putAll" + ) + } + + @Test + fun testIterator() { + val map = SynchronizedMap() + map["a"] = 1 + map["b"] = 2 + val iterator = map.entries.iterator() + assertTrue(iterator.hasNext(), "Iterator should have elements") + val entry1 = iterator.next() + assertEquals("a", entry1.key, "Iterator should return the correct key") + assertEquals(1, entry1.value, "Iterator should return the correct value") + assertTrue(iterator.hasNext(), "Iterator should have more elements") + val entry2 = iterator.next() + assertEquals("b", entry2.key, "Iterator should return the second key") + assertEquals(2, entry2.value, "Iterator should return the second value") + assertFalse(iterator.hasNext(), "Iterator should have no more elements") + } + + @Test + fun testMapCopy() { + val map = SynchronizedMap() + map["key"] = 1 + val copiedMap = map.copy() + assertTrue(copiedMap.containsKey("key"), "Copied map should contain the original key") + assertEquals(1, copiedMap["key"], "Copied map should return the same value for the original key") + assertEquals(1, copiedMap.size, "Copied map should have the same size as the original map") + } +} diff --git a/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedSetTest.kt b/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedSetTest.kt new file mode 100644 index 0000000..15eb5ca --- /dev/null +++ b/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/concurrent/SynchronizedSetTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.teogor.crosslens.core.concurrent + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertFalse + +class SynchronizedSetTest { + + @Test + fun testInitialSize() { + val set = SynchronizedSet() + assertEquals(0, set.size, "Initial size should be 0") + } + + @Test + fun testAddElement() { + val set = SynchronizedSet() + assertTrue(set.add("element"), "Element should be added successfully") + assertEquals(1, set.size, "Size should be 1 after adding an element") + } + + @Test + fun testContainsElement() { + val set = SynchronizedSet() + set.add("element") + assertTrue(set.contains("element"), "Set should contain the added element") + assertFalse(set.contains("nonexistent"), "Set should not contain a non-existent element") + } + + @Test + fun testRemoveElement() { + val set = SynchronizedSet() + set.add("element") + assertTrue(set.remove("element"), "Element should be removed successfully") + assertFalse(set.contains("element"), "Set should not contain the removed element") + } + + @Test + fun testClear() { + val set = SynchronizedSet() + set.add("element") + set.clear() + assertEquals(0, set.size, "Size should be 0 after clearing the set") + assertFalse(set.contains("element"), "Set should not contain any elements after clearing") + } + + @Test + fun testAddAll() { + val set = SynchronizedSet() + val elements = setOf("a", "b", "c") + assertTrue(set.addAll(elements), "All elements should be added successfully") + assertTrue(set.containsAll(elements), "Set should contain all added elements") + } + + @Test + fun testRemoveAll() { + val set = SynchronizedSet() + val elements = setOf("a", "b", "c") + set.addAll(elements) + assertTrue(set.removeAll(elements), "All elements should be removed successfully") + assertFalse(set.containsAll(elements), "Set should not contain removed elements") + } + + @Test + fun testIterator() { + val set = SynchronizedSet() + set.add("a") + set.add("b") + val iterator = set.iterator() + assertTrue(iterator.hasNext(), "Iterator should have elements") + assertEquals("a", iterator.next(), "Iterator should return the first element") + assertTrue(iterator.hasNext(), "Iterator should have more elements") + assertEquals("b", iterator.next(), "Iterator should return the second element") + assertFalse(iterator.hasNext(), "Iterator should have no more elements") + } + + @Test + fun testSetCopy() { + val set = SynchronizedSet() + set.add("element") + val copiedSet = set.copy() + assertTrue(copiedSet.contains("element"), "Copied set should contain the original elements") + assertEquals(1, copiedSet.size, "Copied set should have the same size as the original set") + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3a8b4e..4e49483 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,8 @@ androidx-test-junit = "1.2.1" compose-plugin = "1.6.11" junit = "4.13.2" kotlinx-coroutines = "1.8.1" +kotlinx-collections = "0.3.7" +kotlinx-atomicfu = "0.25.0" dokka = "1.9.20" jetbrains-kotlinx-binary-compatibility = "0.16.3" kotlin = "2.0.20" @@ -39,6 +41,8 @@ androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", nam androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-collections" } +kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinx-atomicfu" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }