diff --git a/common/build.gradle b/common/build.gradle index 9597bd30a20..2ab8e52cd06 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..e62d6f86575 --- /dev/null +++ b/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/common/src/main/kotlin/dev/inmo/micro_utils/common/Mapping.kt b/common/src/main/kotlin/dev/inmo/micro_utils/common/Mapping.kt new file mode 100644 index 00000000000..c9483694bbf --- /dev/null +++ b/common/src/main/kotlin/dev/inmo/micro_utils/common/Mapping.kt @@ -0,0 +1,7 @@ +package dev.inmo.micro_utils.common + +@Suppress("UNCHECKED_CAST", "SimplifiableCall") +inline fun Iterable.mapNotNullA(transform: (T) -> R?): List = map(transform).filter { it != null } as List + +@Suppress("UNCHECKED_CAST", "SimplifiableCall") +inline fun Array.mapNotNullA(mapper: (T) -> R?): List = map(mapper).filter { it != null } as List diff --git a/coroutines/build.gradle b/coroutines/build.gradle index 7616133d4f6..6cfeab046d3 100644 --- a/coroutines/build.gradle +++ b/coroutines/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/coroutines/src/main/AndroidManifest.xml b/coroutines/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..4536ab400ac --- /dev/null +++ b/coroutines/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/defaultAndroidSettings b/defaultAndroidSettings new file mode 100644 index 00000000000..8e8909ff220 --- /dev/null +++ b/defaultAndroidSettings @@ -0,0 +1,31 @@ +android { + compileSdkVersion "$android_compileSdkVersion".toInteger() + buildToolsVersion "$android_buildToolsVersion" + + defaultConfig { + minSdkVersion "$android_minSdkVersion".toInteger() + targetSdkVersion "$android_compileSdkVersion".toInteger() + versionCode "${android_code_version}".toInteger() + versionName "$version" + } + buildTypes { + release { + minifyEnabled false + } + } + + packagingOptions { + exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module' + exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module' + exclude 'META-INF/kotlinx-serialization-properties.kotlin_module' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} diff --git a/extensions.gradle b/extensions.gradle index 8a736a982cb..dd5e93145fa 100644 --- a/extensions.gradle +++ b/extensions.gradle @@ -23,6 +23,8 @@ allprojects { mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject" mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject" + defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings" + publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle" publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle" } diff --git a/gradle.properties b/gradle.properties index d1fd270523a..250c56a1160 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,8 @@ org.gradle.parallel=true kotlin.js.generate.externals=true kotlin.incremental=true kotlin.incremental.js=true +android.useAndroidX=true +android.enableJetifier=true kotlin_version=1.4.10 kotlin_coroutines_version=1.4.1 @@ -20,10 +22,15 @@ uuidVersion=0.2.2 # ANDROID +core_ktx_version=1.3.2 + android_minSdkVersion=24 android_compileSdkVersion=30 android_buildToolsVersion=30.0.2 dexcount_version=2.0.0-RC1 +junit_version=4.12 +test_ext_junit_version=1.1.2 +espresso_core=3.3.0 group=dev.inmo version=0.3.1 diff --git a/ktor/client/build.gradle b/ktor/client/build.gradle index 280a7647124..6983e6f63b4 100644 --- a/ktor/client/build.gradle +++ b/ktor/client/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/ktor/client/src/main/AndroidManifest.xml b/ktor/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..034ad33094d --- /dev/null +++ b/ktor/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ktor/common/build.gradle b/ktor/common/build.gradle index 0d6bb922b88..3413a1a4877 100644 --- a/ktor/common/build.gradle +++ b/ktor/common/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/ktor/common/src/main/AndroidManifest.xml b/ktor/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..9a1797bdc71 --- /dev/null +++ b/ktor/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mime_types/build.gradle b/mime_types/build.gradle index 9597bd30a20..2ab8e52cd06 100644 --- a/mime_types/build.gradle +++ b/mime_types/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/mime_types/src/main/AndroidManifest.xml b/mime_types/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..be8221df297 --- /dev/null +++ b/mime_types/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mppAndroidProject b/mppAndroidProject index e4cb95af6f9..c9bee90b00f 100644 --- a/mppAndroidProject +++ b/mppAndroidProject @@ -21,32 +21,4 @@ kotlin { } } -android { - compileSdkVersion "$android_compileSdkVersion".toInteger() - defaultConfig { - minSdkVersion "$android_minSdkVersion".toInteger() - targetSdkVersion "$android_compileSdkVersion".toInteger() - versionCode "${android_code_version}".toInteger() - versionName "$version" - } - buildTypes { - release { - minifyEnabled false - } - } - - packagingOptions { - exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module' - exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module' - exclude 'META-INF/kotlinx-serialization-properties.kotlin_module' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } -} +apply from: "$defaultAndroidSettingsPresetPath" diff --git a/mppProjectWithSerialization b/mppProjectWithSerialization index 7ee94f9fec3..08ec899fe5d 100644 --- a/mppProjectWithSerialization +++ b/mppProjectWithSerialization @@ -9,6 +9,9 @@ kotlin { browser() nodejs() } + android { + publishLibraryVariants() + } sourceSets { commonMain { @@ -34,5 +37,14 @@ kotlin { implementation kotlin('test-junit') } } + androidTest { + dependencies { + implementation kotlin('test-junit') + implementation "androidx.test.ext:junit:$test_ext_junit_version" + implementation "androidx.test.espresso:espresso-core:$espresso_core" + } + } } } + +apply from: "$defaultAndroidSettingsPresetPath" diff --git a/pagination/common/build.gradle b/pagination/common/build.gradle index 9597bd30a20..2ab8e52cd06 100644 --- a/pagination/common/build.gradle +++ b/pagination/common/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/pagination/common/src/main/AndroidManifest.xml b/pagination/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..3d678ce1cad --- /dev/null +++ b/pagination/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pagination/ktor/common/build.gradle b/pagination/ktor/common/build.gradle index dfbe880de3d..18c9967a1ab 100644 --- a/pagination/ktor/common/build.gradle +++ b/pagination/ktor/common/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/pagination/ktor/common/src/main/AndroidManifest.xml b/pagination/ktor/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..b2896442be7 --- /dev/null +++ b/pagination/ktor/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/repos/android/build.gradle b/repos/android/build.gradle index d35a26fc165..aa5218eaf68 100644 --- a/repos/android/build.gradle +++ b/repos/android/build.gradle @@ -6,3 +6,19 @@ plugins { } apply from: "$mppAndroidProjectPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api internalProject("micro_utils.repos.common") + api internalProject("micro_utils.coroutines") + } + } + androidMain { + dependencies { + implementation "androidx.core:core-ktx:$core_ktx_version" + } + } + } +} diff --git a/repos/android/src/main/AndroidManifest.xml b/repos/android/src/main/AndroidManifest.xml index 49151ff7948..cbd8db25bb7 100644 --- a/repos/android/src/main/AndroidManifest.xml +++ b/repos/android/src/main/AndroidManifest.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/repos/common/build.gradle b/repos/common/build.gradle index a36a04e5b95..e78548f9238 100644 --- a/repos/common/build.gradle +++ b/repos/common/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" @@ -15,5 +17,13 @@ kotlin { api "com.benasher44:uuid:$uuidVersion" } } + + androidMain { + dependencies { + api "androidx.core:core-ktx:$core_ktx_version" + api internalProject("micro_utils.common") + api internalProject("micro_utils.coroutines") + } + } } } \ No newline at end of file diff --git a/repos/common/src/main/AndroidManifest.xml b/repos/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..66982e66339 --- /dev/null +++ b/repos/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ColumnType.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ColumnType.kt new file mode 100644 index 00000000000..b2026a216f5 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ColumnType.kt @@ -0,0 +1,46 @@ +package dev.inmo.micro_utils.repos + +sealed class ColumnType( + typeName: String, + nullable: Boolean +) { + open val asType: String = "$typeName${if (!nullable) " not null" else ""}" + sealed class Text( + nullable: Boolean + ) : ColumnType("text", nullable) { + object NULLABLE : Text(true) + object NOT_NULLABLE : Text(false) + } + sealed class Numeric( + typeName: String, + autoincrement: Boolean = false, + primaryKey: Boolean = false, + nullable: Boolean = false + ) : ColumnType( + typeName, + nullable + ) { + override val asType: String = "${super.asType}${if (primaryKey) " primary key" else ""}${if (autoincrement) " autoincrement" else ""}" + + class INTEGER( + autoincrement: Boolean = false, + primaryKey: Boolean = false, + nullable: Boolean = false + ) : Numeric( + "integer", + autoincrement, + primaryKey, + nullable + ) + class DOUBLE(autoincrement: Boolean = false, primaryKey: Boolean = false, nullable: Boolean = false) : Numeric( + "double", + autoincrement, + primaryKey, + nullable + ) + } + + override fun toString(): String { + return asType + } +} \ No newline at end of file diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ContentValuesOf.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ContentValuesOf.kt new file mode 100644 index 00000000000..44b80bd7dc1 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ContentValuesOf.kt @@ -0,0 +1,8 @@ +package dev.inmo.micro_utils.repos + +import androidx.core.content.contentValuesOf + +@Suppress("UNCHECKED_CAST", "SimplifiableCall") +fun contentValuesOfNotNull(vararg pairs: Pair?) = contentValuesOf( + *(pairs.filter { it != null } as List>).toTypedArray() +) diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/CursorMapping.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/CursorMapping.kt new file mode 100644 index 00000000000..e74374ac802 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/CursorMapping.kt @@ -0,0 +1,21 @@ +package dev.inmo.micro_utils.repos + +import android.database.Cursor + +inline fun Cursor.map( + block: (Cursor) -> T +): List { + val result = mutableListOf() + if (moveToFirst()) { + do { + result.add(block(this)) + } while (moveToNext()) + } + return result +} + +fun Cursor.firstOrNull(): Cursor? = if (moveToFirst()) { + this +} else { + null +} diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt new file mode 100644 index 00000000000..8a2932eb0fe --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt @@ -0,0 +1,6 @@ +package dev.inmo.micro_utils.repos + +import kotlinx.coroutines.newSingleThreadContext +import kotlin.coroutines.CoroutineContext + +val DatabaseCoroutineContext: CoroutineContext = newSingleThreadContext("db-context") diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseOperations.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseOperations.kt new file mode 100644 index 00000000000..b1cfdb28804 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseOperations.kt @@ -0,0 +1,64 @@ +package dev.inmo.micro_utils.repos + +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase + +fun createTableQuery( + tableName: String, + vararg columnsToTypes: Pair +) = "create table $tableName (${columnsToTypes.joinToString(", ") { "${it.first} ${it.second}" }});" + +fun SQLiteDatabase.createTable( + tableName: String, + vararg columnsToTypes: Pair, + onInit: (SQLiteDatabase.() -> Unit)? = null +): Boolean { + val existing = rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName'", null).use { + it.count > 0 + } + return if (existing) { + false + // TODO:: add upgrade opportunity + } else { + execSQL(createTableQuery(tableName, *columnsToTypes)) + onInit ?.invoke(this) + true + } +} + +fun Cursor.getString(columnName: String) = getString( + getColumnIndex(columnName) +) + +fun Cursor.getLong(columnName: String) = getLong( + getColumnIndex(columnName) +) + +fun Cursor.getInt(columnName: String) = getInt( + getColumnIndex(columnName) +) + +fun Cursor.getDouble(columnName: String) = getDouble( + getColumnIndex(columnName) +) + +fun SQLiteDatabase.select( + table: String, + columns: Array? = null, + selection: String? = null, + selectionArgs: Array? = null, + groupBy: String? = null, + having: String? = null, + orderBy: String? = null, + limit: String? = null +) = query( + table, columns, selection, selectionArgs, groupBy, having, orderBy, limit +) + +fun makePlaceholders(count: Int): String { + return (0 until count).joinToString { "?" } +} + +fun makeStringPlaceholders(count: Int): String { + return (0 until count).joinToString { "\"?\"" } +} diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabasePaginationExtensions.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabasePaginationExtensions.kt new file mode 100644 index 00000000000..df72f0b4b61 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabasePaginationExtensions.kt @@ -0,0 +1,8 @@ +package dev.inmo.micro_utils.repos + +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.firstIndex + +fun limitClause(size: Long, since: Long? = null) = "${since ?.let { "$it, " } ?: ""}$size" +fun limitClause(size: Int, since: Int? = null) = limitClause(size.toLong(), since ?.toLong()) +fun Pagination.limitClause() = limitClause(size, firstIndex) diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt new file mode 100644 index 00000000000..1ac58f237a5 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt @@ -0,0 +1,45 @@ +package dev.inmo.micro_utils.repos + +import android.database.sqlite.SQLiteDatabase +import dev.inmo.micro_utils.coroutines.safely +import kotlinx.coroutines.withContext + +suspend fun SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T): T { + return withContext(DatabaseCoroutineContext) { + when { + inTransaction() -> { + block() + } + else -> { + beginTransaction() + safely( + { + endTransaction() + throw it + } + ) { + block().also { + setTransactionSuccessful() + endTransaction() + } + } + } + } + } +} + +inline fun SQLiteDatabase.inlineTransaction(block: SQLiteDatabase.() -> T): T { + return when { + inTransaction() -> block() + else -> { + beginTransaction() + try { + block().also { + setTransactionSuccessful() + } + } finally { + endTransaction() + } + } + } +} diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/InternalId.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/InternalId.kt new file mode 100644 index 00000000000..c95db5f7ab9 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/InternalId.kt @@ -0,0 +1,7 @@ +package dev.inmo.micro_utils.repos + +val internalId = "_id" +val internalIdType = ColumnType.Numeric.INTEGER( + autoincrement = true, + primaryKey = true +) diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt new file mode 100644 index 00000000000..8de4676138a --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt @@ -0,0 +1,51 @@ +package dev.inmo.micro_utils.repos + +import android.content.Context +import android.database.DatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import dev.inmo.micro_utils.coroutines.safely +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +private data class CallbackContinuationPair ( + val callback: suspend SQLiteDatabase.() -> T, + val continuation: Continuation +) { + suspend fun SQLiteDatabase.execute() { + safely( + { + continuation.resumeWithException(it) + } + ) { + continuation.resume(callback()) + } + } +} + +class StandardSQLHelper( + context: Context, + name: String, + factory: SQLiteDatabase.CursorFactory? = null, + version: Int = 1, + errorHandler: DatabaseErrorHandler? = null +) { + val sqlOpenHelper = object : SQLiteOpenHelper(context, name, factory, version, errorHandler) { + override fun onCreate(db: SQLiteDatabase?) {} + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {} + } + + suspend fun writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block) + + suspend fun readableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.readableTransaction(block) +} + +suspend fun SQLiteOpenHelper.writableTransaction(block: suspend SQLiteDatabase.() -> T): T { + return writableDatabase.transaction(block) +} + +suspend fun SQLiteOpenHelper.readableTransaction(block: suspend SQLiteDatabase.() -> T): T { + return readableDatabase.transaction(block) +} diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractAndroidCRUDRepo.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractAndroidCRUDRepo.kt new file mode 100644 index 00000000000..bd1109b71ea --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractAndroidCRUDRepo.kt @@ -0,0 +1,67 @@ +package dev.inmo.micro_utils.repos.crud + +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.pagination.createPaginationResult +import dev.inmo.micro_utils.repos.* + +val T.asId: String + get() = (this as? String) ?: this!!.toString() + +abstract class AbstractAndroidCRUDRepo( + protected val helper: StandardSQLHelper +) : ReadStandardCRUDRepo { + protected abstract val tableName: String + protected abstract val idColumnName: String + protected abstract suspend fun Cursor.toObject(): ObjectType + protected fun SQLiteDatabase.count(): Long = select(tableName).use { + it.count + }.toLong() + + override suspend fun contains(id: IdType): Boolean = helper.readableTransaction { + select( + tableName, + null, + "$idColumnName=?", + arrayOf(id.asId) + ).use { + it.count > 0 + } + } + + override suspend fun getById(id: IdType): ObjectType? = helper.readableTransaction { + select( + tableName, + selection = "$idColumnName=?", + selectionArgs = arrayOf(id.asId), + limit = limitClause(1) + ).use { c -> + if (c.moveToFirst()) { + c.toObject() + } else { + null + } + } + } + + override suspend fun getByPagination(pagination: Pagination): PaginationResult { + return helper.readableTransaction { + select( + tableName, + limit = pagination.limitClause() + ).use { + if (it.moveToFirst()) { + val resultList = mutableListOf(it.toObject()) + while (it.moveToNext()) { + resultList.add(it.toObject()) + } + resultList.createPaginationResult(pagination, count()) + } else { + emptyList().createPaginationResult(pagination, 0) + } + } + } + } +} \ No newline at end of file diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractMutableAndroidCRUDRepo.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractMutableAndroidCRUDRepo.kt new file mode 100644 index 00000000000..83bab7ad2b9 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractMutableAndroidCRUDRepo.kt @@ -0,0 +1,104 @@ +package dev.inmo.micro_utils.repos.crud + +import android.content.ContentValues +import dev.inmo.micro_utils.common.mapNotNullA +import dev.inmo.micro_utils.repos.* +import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow + +abstract class AbstractMutableAndroidCRUDRepo( + helper: StandardSQLHelper +) : WriteStandardCRUDRepo, + AbstractAndroidCRUDRepo(helper) { + protected val newObjectsChannel = BroadcastChannel(64) + protected val updateObjectsChannel = BroadcastChannel(64) + protected val deleteObjectsIdsChannel = BroadcastChannel(64) + override val newObjectsFlow: Flow = newObjectsChannel.asFlow() + override val updatedObjectsFlow: Flow = updateObjectsChannel.asFlow() + override val deletedObjectsIdsFlow: Flow = deleteObjectsIdsChannel.asFlow() + + protected abstract suspend fun InputValueType.asContentValues(id: IdType? = null): ContentValues + + override suspend fun create(values: List): List { + val indexes = helper.writableTransaction { + values.map { + insert(tableName, null, it.asContentValues()) + } + } + return helper.readableTransaction { + indexes.mapNotNullA { i -> + select( + tableName, + selection = "$internalId=?", + selectionArgs = arrayOf(i.toString()) + ).use { c -> + if (c.moveToFirst()) { + c.toObject() + } else { + null + } + } + } + }.also { + it.forEach { + newObjectsChannel.send(it) + } + } + } + + override suspend fun deleteById(ids: List) { + val deleted = mutableListOf() + helper.writableTransaction { + ids.forEach { id -> + delete(tableName, "$idColumnName=?", arrayOf(id.asId)).also { + if (it > 0) { + deleted.add(id) + } + } + } + } + deleted.forEach { + deleteObjectsIdsChannel.send(it) + } + } + + override suspend fun update(id: IdType, value: InputValueType): ObjectType? { + val asContentValues = value.asContentValues(id) + if (asContentValues.keySet().isNotEmpty()) { + helper.writableTransaction { + update( + tableName, + asContentValues, + "$idColumnName=?", + arrayOf(id.asId) + ) + } + } + return getById(id) ?.also { + updateObjectsChannel.send(it) + } + } + + override suspend fun update(values: List>): List { + helper.writableTransaction { + values.forEach { (id, value) -> + update( + tableName, + value.asContentValues(id), + "$idColumnName=?", + arrayOf(id.asId) + ) + } + } + return values.mapNotNullA { + getById(it.first) + }.also { + it.forEach { + updateObjectsChannel.send(it) + } + } + } + + override suspend fun count(): Long = helper.readableTransaction { select(tableName).use { it.count.toLong() } } +} \ No newline at end of file diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/keyvalue/KeyValueStore.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/keyvalue/KeyValueStore.kt new file mode 100644 index 00000000000..bcf16a4f7a6 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/keyvalue/KeyValueStore.kt @@ -0,0 +1,130 @@ +package dev.inmo.micro_utils.repos.keyvalue + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.pagination.utils.paginate +import dev.inmo.micro_utils.pagination.utils.reverse +import dev.inmo.micro_utils.repos.StandardKeyValueRepo +import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow + +private val cache = HashMap>() + +fun Context.keyValueStore( + name: String = "default", + cacheValues: Boolean = false +): StandardKeyValueRepo { + return cache.getOrPut(name) { + KeyValueStore(this, name, cacheValues) + } as KeyValueStore +} + +class KeyValueStore internal constructor ( + c: Context, + preferencesName: String, + useCache: Boolean = false +) : SharedPreferences.OnSharedPreferenceChangeListener, StandardKeyValueRepo { + private val sharedPreferences = c.getSharedPreferences(preferencesName, Context.MODE_PRIVATE) + + private val cachedData = if (useCache) { + mutableMapOf() + } else { + null + } + + private val onNewValueChannel = BroadcastChannel>(Channel.BUFFERED) + private val onValueRemovedChannel = BroadcastChannel(Channel.BUFFERED) + + override val onNewValue: Flow> = onNewValueChannel.asFlow() + override val onValueRemoved: Flow = onValueRemovedChannel.asFlow() + + init { + cachedData ?.let { + sharedPreferences.all.forEach { + if (it.value != null) { + cachedData[it.key] = it.value as Any + } + } + sharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + } + + override fun onSharedPreferenceChanged(sp: SharedPreferences, key: String) { + val value = sp.all[key] + cachedData ?: return + if (value != null) { + cachedData[key] = value + } else { + cachedData.remove(key) + } + } + + override suspend fun get(k: String): T? { + return (cachedData ?. get(k) ?: sharedPreferences.all[k]) as? T + } + + override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult { + val resultPagination = if (reversed) pagination.reverse(count()) else pagination + return sharedPreferences.all.values.paginate( + resultPagination + ).let { + PaginationResult( + it.page, + it.pagesNumber, + it.results.map { it as T }.let { if (reversed) it.reversed() else it }, + it.size + ) + } + } + + override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult { + val resultPagination = if (reversed) pagination.reverse(count()) else pagination + return sharedPreferences.all.keys.paginate( + resultPagination + ).let { + PaginationResult( + it.page, + it.pagesNumber, + it.results.let { if (reversed) it.reversed() else it }, + it.size + ) + } + } + + override suspend fun contains(key: String): Boolean = sharedPreferences.contains(key) + + override suspend fun count(): Long = sharedPreferences.all.size.toLong() + + override suspend fun set(toSet: Map) { + sharedPreferences.edit { + toSet.forEach { (k, v) -> + when(v) { + is Int -> putInt(k, v) + is Long -> putLong(k, v) + is Float -> putFloat(k, v) + is String -> putString(k, v) + is Boolean -> putBoolean(k, v) + is Set<*> -> putStringSet(k, v.map { (it as? String) ?: it.toString() }.toSet()) + else -> error( + "Currently supported only primitive types and set for SharedPreferences KeyValue repos" + ) + } + } + } + toSet.forEach { (k, v) -> + onNewValueChannel.send(k to v) + } + } + + override suspend fun unset(toUnset: List) { + sharedPreferences.edit { + toUnset.forEach { remove(it) } + } + toUnset.forEach { onValueRemovedChannel.send(it) } + } +} diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/onetomany/OneToManyAndroidRepo.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/onetomany/OneToManyAndroidRepo.kt new file mode 100644 index 00000000000..de4b6f22078 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/onetomany/OneToManyAndroidRepo.kt @@ -0,0 +1,187 @@ +package dev.inmo.micro_utils.repos.onetomany + +import android.database.sqlite.SQLiteOpenHelper +import androidx.core.content.contentValuesOf +import dev.inmo.micro_utils.common.mapNotNullA +import dev.inmo.micro_utils.pagination.FirstPagePagination +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.pagination.createPaginationResult +import dev.inmo.micro_utils.pagination.utils.reverse +import dev.inmo.micro_utils.repos.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json + +private val internalSerialFormat = Json { + ignoreUnknownKeys = true +} + +class OneToManyAndroidRepo( + private val tableName: String, + private val keySerializer: KSerializer, + private val valueSerializer: KSerializer, + private val helper: SQLiteOpenHelper +) : OneToManyKeyValueRepo { + private val _onNewValue: MutableSharedFlow> = MutableSharedFlow() + override val onNewValue: Flow> = _onNewValue.asSharedFlow() + private val _onValueRemoved: MutableSharedFlow> = MutableSharedFlow() + override val onValueRemoved: Flow> = _onValueRemoved.asSharedFlow() + private val _onDataCleared = MutableSharedFlow() + override val onDataCleared: Flow = _onDataCleared.asSharedFlow() + + private val idColumnName = "id" + private val valueColumnName = "value" + + private fun Key.asId() = internalSerialFormat.encodeToString(keySerializer, this) + private fun Value.asValue() = internalSerialFormat.encodeToString(valueSerializer, this) + private fun String.asValue(): Value = internalSerialFormat.decodeFromString(valueSerializer, this) + private fun String.asKey(): Key = internalSerialFormat.decodeFromString(keySerializer, this) + + init { + runBlocking(DatabaseCoroutineContext) { + helper.writableTransaction { + createTable( + tableName, + internalId to internalIdType, + idColumnName to ColumnType.Text.NOT_NULLABLE, + valueColumnName to ColumnType.Text.NULLABLE + ) + } + } + } + + override suspend fun add(toAdd: Map>) { + val added = mutableListOf>() + helper.writableTransaction { + toAdd.forEach { (k, values) -> + values.forEach { v -> + insert( + tableName, + null, + contentValuesOf( + idColumnName to k.asId(), + valueColumnName to v.asValue() + ) + ).also { + if (it != -1L) { + added.add(k to v) + } + } + } + } + } + added.forEach { _onNewValue.emit(it) } + } + + override suspend fun clear(k: Key) { + helper.writableTransaction { + delete(tableName, "$idColumnName=?", arrayOf(k.asId())) + }.also { + if (it > 0) { + _onDataCleared.emit(k) + } + } + } + + override suspend fun contains(k: Key): Boolean = helper.readableTransaction { + select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use { + it.count > 0 + } + } + + override suspend fun contains(k: Key, v: Value): Boolean = helper.readableTransaction { + select( + tableName, + selection = "$idColumnName=? AND $valueColumnName=?", + selectionArgs = arrayOf(k.asId(), v.asValue()), + limit = FirstPagePagination(1).limitClause() + ).use { + it.count > 0 + } + } + + override suspend fun count(): Long =helper.readableTransaction { + select( + tableName + ).use { + it.count + } + }.toLong() + + override suspend fun count(k: Key): Long = helper.readableTransaction { + select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use { + it.count + } + }.toLong() + + override suspend fun get( + k: Key, + pagination: Pagination, + reversed: Boolean + ): PaginationResult = count(k).let { count -> + val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination } + helper.readableTransaction { + select( + tableName, + selection = "$idColumnName=?", + selectionArgs = arrayOf(k.asId()), + limit = resultPagination.limitClause() + ).use { c -> + mutableListOf().also { + if (c.moveToFirst()) { + do { + it.add(c.getString(valueColumnName).asValue()) + } while (c.moveToNext()) + } + } + } + }.createPaginationResult( + pagination, + count + ) + } + + override suspend fun keys( + pagination: Pagination, + reversed: Boolean + ): PaginationResult = count().let { count -> + val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination } + helper.readableTransaction { + select( + tableName, + limit = resultPagination.limitClause() + ).use { c -> + mutableListOf().also { + if (c.moveToFirst()) { + do { + it.add(c.getString(idColumnName).asKey()) + } while (c.moveToNext()) + } + } + } + }.createPaginationResult( + pagination, + count + ) + } + + override suspend fun remove(toRemove: Map>) { + helper.writableTransaction { + toRemove.flatMap { (k, vs) -> + vs.mapNotNullA { v -> + if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.asId(), v.asValue())) > 0) { + k to v + } else { + null + } + } + } + }.forEach { (k, v) -> + _onValueRemoved.emit(k to v) + } + } +} diff --git a/repos/inmemory/build.gradle b/repos/inmemory/build.gradle index 162e4af700e..2e766c6966f 100644 --- a/repos/inmemory/build.gradle +++ b/repos/inmemory/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/repos/inmemory/src/main/AndroidManifest.xml b/repos/inmemory/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..63948d05fbf --- /dev/null +++ b/repos/inmemory/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/repos/ktor/client/build.gradle b/repos/ktor/client/build.gradle index 2076deebca3..30fea42d54c 100644 --- a/repos/ktor/client/build.gradle +++ b/repos/ktor/client/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/repos/ktor/client/src/main/AndroidManifest.xml b/repos/ktor/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..a6de7e9b159 --- /dev/null +++ b/repos/ktor/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/repos/ktor/common/build.gradle b/repos/ktor/common/build.gradle index 506faf0b021..7449b67d2fb 100644 --- a/repos/ktor/common/build.gradle +++ b/repos/ktor/common/build.gradle @@ -1,6 +1,8 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "kotlin-android-extensions" } apply from: "$mppProjectWithSerializationPresetPath" diff --git a/repos/ktor/common/src/main/AndroidManifest.xml b/repos/ktor/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..df34e12f6a0 --- /dev/null +++ b/repos/ktor/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 3a4bff38743..4bfb57df815 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,7 +13,6 @@ String[] includes = [ ":repos:ktor:client", ":repos:ktor:common", ":repos:ktor:server", - ":repos:android", ":ktor:server", ":ktor:common", ":ktor:client",