Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic Storage interface for use on all client and on the server. #834

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use iosMain, iosTest instead of appleMain, appleTest? I think that's more correct since we only target iOS not MacOS...

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
Loading