Skip to content

Commit

Permalink
Generic Storage interface for use on all client and on the server. (#834
Browse files Browse the repository at this point in the history
)

Signed-off-by: Peter Sorotokin <[email protected]>
  • Loading branch information
sorotokin authored Jan 10, 2025
1 parent dcb6a06 commit 038644d
Show file tree
Hide file tree
Showing 29 changed files with 2,542 additions and 16 deletions.
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ zxing = "3.5.3"
gretty = "4.1.4"
hsqldb = "2.7.2"
mysql = "8.0.16"
postgresql = "42.7.4"
compose-junit4 = "1.6.8"
compose-test-manifest = "1.6.8"
androidx-fragment = "1.8.0"
Expand All @@ -61,6 +62,7 @@ cameraLifecycle = "1.3.4"
buildconfig = "5.3.5"
qrose = "1.0.1"
easyqrscan = "0.2.0"
androidx-sqlite = "2.5.0-alpha12"

[libraries]
face-detection = { module = "com.google.mlkit:face-detection", version.ref = "faceDetection" }
Expand Down Expand Up @@ -135,6 +137,10 @@ jetbrains-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifec
camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycle" }
qrose = { group = "io.github.alexzhirkevich", name = "qrose", version.ref="qrose"}
easyqrscan = { module = "io.github.kalinjul.easyqrscan:scanner", version.ref = "easyqrscan" }
androidx-sqlite = { module="androidx.sqlite:sqlite", version.ref = "androidx-sqlite" }
androidx-sqlite-framework = { module="androidx.sqlite:sqlite-framework", version.ref = "androidx-sqlite" }
androidx-sqlite-bundled = { module="androidx.sqlite:sqlite-bundled", version.ref = "androidx-sqlite" }
postgresql = { module="org.postgresql:postgresql", version.ref = "postgresql" }

[bundles]
google-play-services = ["play-services-base", "play-services-basement", "play-services-tasks"]
Expand Down
30 changes: 30 additions & 0 deletions identity/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,44 @@ kotlin {
}
}

val appleMain by getting {
dependencies {
// This dependency is needed for SqliteStorage implementation.
// KMP-compatible version is still alpha and it is not compatible with
// other androidx packages, particularly androidx.work that we use in wallet.
// TODO: once compatibility issues are resolved, SqliteStorage and this
// dependency can be moved into commonMain.
implementation(libs.androidx.sqlite)
}
}

val jvmTest by getting {
dependencies {
implementation(libs.hsqldb)
implementation(libs.mysql)
implementation(libs.postgresql)
}
}

val androidInstrumentedTest by getting {
dependsOn(commonTest)
dependencies {
implementation(libs.androidx.sqlite)
implementation(libs.androidx.sqlite.framework)
implementation(libs.androidx.sqlite.bundled)
implementation(libs.androidx.test.junit)
implementation(libs.androidx.espresso.core)
implementation(libs.compose.junit4)
}
}

val appleTest by getting {
dependencies {
implementation(libs.androidx.sqlite)
implementation(libs.androidx.sqlite.framework)
implementation(libs.androidx.sqlite.bundled)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.android.identity.storage

import android.app.Instrumentation
import androidx.test.platform.app.InstrumentationRegistry
import com.android.identity.storage.android.AndroidStorage
import com.android.identity.storage.ephemeral.EphemeralStorage
import kotlinx.datetime.Clock
import java.io.File

/**
* Creates a list of empty [Storage] objects for testing.
*/
actual fun createTransientStorageList(testClock: Clock): List<Storage> {
return listOf<Storage>(
EphemeralStorage(testClock),
/*
TODO: this can be enabled once SqliteStorage is moved into commonMain
com.android.identity.storage.sqlite.SqliteStorage(
connection = AndroidSQLiteDriver().open(":memory:"),
clock = testClock
),
com.android.identity.storage.sqlite.SqliteStorage(
connection = BundledSQLiteDriver().open(":memory:"),
clock = testClock,
// bundled sqlite crashes when used with Dispatchers.IO
coroutineContext = newSingleThreadContext("DB")
),
*/
AndroidStorage(
databasePath = null,
clock = testClock,
keySize = 3
)
)
}

val knownNames = mutableSetOf<String>()

actual fun createPersistentStorage(name: String, testClock: Clock): Storage? {
val context = InstrumentationRegistry.getInstrumentation().context
val dbFile = context.getDatabasePath("$name.db")
if (knownNames.add(name)) {
dbFile.delete()
}
return AndroidStorage(
databasePath = dbFile.absolutePath,
clock = testClock,
keySize = 3
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.android.identity.storage.android

import android.database.sqlite.SQLiteDatabase
import com.android.identity.storage.Storage
import com.android.identity.storage.base.BaseStorage
import com.android.identity.storage.base.BaseStorageTable
import com.android.identity.storage.StorageTableSpec
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.datetime.Clock
import kotlin.coroutines.CoroutineContext

/**
* [Storage] implementation based on Android [SQLiteDatabase] API.
*/
class AndroidStorage: BaseStorage {
private val coroutineContext: CoroutineContext
private val databaseFactory: () -> SQLiteDatabase
internal val keySize: Int
private var database: SQLiteDatabase? = null

constructor(
database: SQLiteDatabase,
clock: Clock,
coroutineContext: CoroutineContext = Dispatchers.IO,
keySize: Int = 9
): super(clock) {
this.database = database
databaseFactory = { throw IllegalStateException("unexpected call") }
this.coroutineContext = coroutineContext
this.keySize = keySize
}

constructor(
databasePath: String?,
clock: Clock,
coroutineContext: CoroutineContext = Dispatchers.IO,
keySize: Int = 9
): super(clock) {
databaseFactory = {
SQLiteDatabase.openOrCreateDatabase(databasePath ?: ":memory:", null)
}
this.coroutineContext = coroutineContext
this.keySize = keySize
}

override suspend fun createTable(tableSpec: StorageTableSpec): BaseStorageTable {
if (database == null) {
database = databaseFactory()
}
val table = AndroidStorageTable(this, tableSpec)
table.init()
return table
}

internal suspend fun<T> withDatabase(
block: suspend CoroutineScope.(database: SQLiteDatabase) -> T
): T {
return CoroutineScope(coroutineContext).async {
block(database!!)
}.await()
}
}
Loading

0 comments on commit 038644d

Please sign in to comment.