Add a CacheStore module.
This commit is contained in:
parent
c1e908a8e6
commit
29e0a08dd9
15 changed files with 374 additions and 0 deletions
13
libraries/cachestore/api/build.gradle.kts
Normal file
13
libraries/cachestore/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.cachestore.api"
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.cachestore.api
|
||||
|
||||
data class CacheData(
|
||||
val value: String,
|
||||
val updatedAt: Long,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.cachestore.api
|
||||
|
||||
interface CacheStore {
|
||||
suspend fun storeData(key: String, data: CacheData)
|
||||
suspend fun getData(key: String): CacheData?
|
||||
suspend fun deleteData(key: String)
|
||||
}
|
||||
48
libraries/cachestore/impl/build.gradle.kts
Normal file
48
libraries/cachestore/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.sqldelight)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.cachestore.impl"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.encryptedDb)
|
||||
api(projects.libraries.cachestore.api)
|
||||
implementation(libs.sqldelight.driver.android)
|
||||
implementation(libs.sqlcipher)
|
||||
implementation(libs.sqlite)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(libs.sqldelight.coroutines)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.sqldelight.driver.jvm)
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
databases {
|
||||
create("CacheDatabase") {
|
||||
// https://sqldelight.github.io/sqldelight/2.1.0/android_sqlite/migrations/
|
||||
// To generate a .db file from your latest schema, run this task
|
||||
// ./gradlew generateDebugCacheDatabaseSchema
|
||||
// Test migration by running
|
||||
// ./gradlew verifySqlDelightMigration
|
||||
schemaOutputDirectory = File("src/main/sqldelight/databases")
|
||||
verifyMigrations = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.cachestore.impl
|
||||
|
||||
import io.element.android.libraries.cachestore.api.CacheData
|
||||
import java.util.Date
|
||||
import io.element.android.libraries.cachestore.CacheData as DbCacheData
|
||||
|
||||
internal fun CacheData.toDbModel(key: String): DbCacheData {
|
||||
return DbCacheData(
|
||||
key = key,
|
||||
value_ = value,
|
||||
updatedAt = updatedAt.time,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DbCacheData.toApiModel(): CacheData {
|
||||
return CacheData(
|
||||
value = value_,
|
||||
updatedAt = Date(updatedAt),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.cachestore.impl
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.cachestore.api.CacheData
|
||||
import io.element.android.libraries.cachestore.api.CacheStore
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DatabaseCacheStore(
|
||||
private val database: CacheDatabase,
|
||||
) : CacheStore {
|
||||
override suspend fun getData(key: String): CacheData? {
|
||||
return database.cacheDataQueries.selectData(key)
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
}
|
||||
|
||||
override suspend fun storeData(key: String, data: CacheData) {
|
||||
database.cacheDataQueries.insertData(
|
||||
data.toDbModel(key)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteData(key: String) {
|
||||
database.cacheDataQueries.deleteData(key)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.cachestore.impl.di
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Provides
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.cachestore.impl.CacheDatabase
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.encrypteddb.SqlCipherDriverFactory
|
||||
import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider
|
||||
|
||||
@BindingContainer
|
||||
@ContributesTo(AppScope::class)
|
||||
object CacheStoreModule {
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun provideCacheDatabase(
|
||||
@ApplicationContext context: Context,
|
||||
): CacheDatabase {
|
||||
val name = "cache_database"
|
||||
val secretFile = context.getDatabasePath("$name.key")
|
||||
|
||||
// Make sure the parent directory of the key file exists, otherwise it will crash in older Android versions
|
||||
val parentDir = secretFile.parentFile
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs()
|
||||
}
|
||||
|
||||
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile)
|
||||
val driver = SqlCipherDriverFactory(passphraseProvider)
|
||||
.create(CacheDatabase.Schema, "$name.db", context)
|
||||
return CacheDatabase(driver)
|
||||
}
|
||||
}
|
||||
BIN
libraries/cachestore/impl/src/main/sqldelight/databases/1.db
Normal file
BIN
libraries/cachestore/impl/src/main/sqldelight/databases/1.db
Normal file
Binary file not shown.
|
|
@ -0,0 +1,26 @@
|
|||
--------------------------------------------------------------------
|
||||
-- Current version of the DB is the highest value of filename
|
||||
-- in the folder `sqldelight/databases`.
|
||||
--
|
||||
-- When upgrading the schema, you have to create a file .sqm in the
|
||||
-- `sqldelight/databases` folder and run the following task to
|
||||
-- generate a .db file using the latest schema
|
||||
-- > ./gradlew generateDebugCacheDatabaseSchema
|
||||
--------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE CacheData (
|
||||
key TEXT NOT NULL PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updatedAt INTEGER NOT NULL
|
||||
);
|
||||
|
||||
|
||||
selectData:
|
||||
SELECT * FROM CacheData WHERE key = ?;
|
||||
|
||||
-- insert or update data by key
|
||||
insertData:
|
||||
INSERT INTO CacheData VALUES ? ON CONFLICT(key) DO UPDATE SET value = excluded.value, updatedAt = excluded.updatedAt;
|
||||
|
||||
deleteData:
|
||||
DELETE FROM CacheData WHERE key = ?;
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.sessionstorage.impl
|
||||
|
||||
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.cachestore.api.CacheData
|
||||
import io.element.android.libraries.cachestore.impl.CacheDatabase
|
||||
import io.element.android.libraries.cachestore.impl.DatabaseCacheStore
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import io.element.android.libraries.cachestore.CacheData as DbCacheData
|
||||
|
||||
private const val A_KEY = "aKey"
|
||||
private const val A_DATA_1 = "aData1"
|
||||
private const val A_DATA_2 = "aData2"
|
||||
|
||||
class DatabaseCacheStoreTest {
|
||||
private lateinit var database: CacheDatabase
|
||||
private lateinit var databaseCacheStore: DatabaseCacheStore
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Before
|
||||
fun setup() {
|
||||
// Initialise in memory SQLite driver
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
CacheDatabase.Schema.create(driver)
|
||||
|
||||
database = CacheDatabase(driver)
|
||||
databaseCacheStore = DatabaseCacheStore(
|
||||
database = database,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeData persists the CacheData into the DB, deleteData deletes it`() = runTest {
|
||||
// Assert that no data is stored for the key
|
||||
assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull()
|
||||
// Store data
|
||||
databaseCacheStore.storeData(A_KEY, CacheData(A_DATA_1, 1))
|
||||
assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isEqualTo(
|
||||
DbCacheData(
|
||||
key = A_KEY,
|
||||
value_ = A_DATA_1,
|
||||
updatedAt = 1,
|
||||
)
|
||||
)
|
||||
// Update data
|
||||
databaseCacheStore.storeData(A_KEY, CacheData(A_DATA_2, 2))
|
||||
assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isEqualTo(
|
||||
DbCacheData(
|
||||
key = A_KEY,
|
||||
value_ = A_DATA_2,
|
||||
updatedAt = 2,
|
||||
)
|
||||
)
|
||||
// Delete data
|
||||
databaseCacheStore.deleteData(A_KEY)
|
||||
assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.sessionstorage.impl
|
||||
|
||||
import io.element.android.libraries.cachestore.CacheData
|
||||
import java.util.Date
|
||||
|
||||
internal fun aCacheData(
|
||||
key: String = "aKey",
|
||||
value: String = "aValue",
|
||||
updatedAt: Date = Date(),
|
||||
) = CacheData(
|
||||
key = key,
|
||||
value_ = value,
|
||||
updatedAt = updatedAt.time,
|
||||
)
|
||||
17
libraries/cachestore/test/build.gradle.kts
Normal file
17
libraries/cachestore/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.cachestore.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.cachestore.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.sessionstorage.test
|
||||
|
||||
import io.element.android.libraries.cachestore.api.CacheData
|
||||
|
||||
fun aCacheData(
|
||||
value: String = "aValue",
|
||||
updatedAt: Long = 0,
|
||||
) = CacheData(
|
||||
value = value,
|
||||
updatedAt = updatedAt,
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.sessionstorage.test
|
||||
|
||||
import io.element.android.libraries.cachestore.api.CacheData
|
||||
import io.element.android.libraries.cachestore.api.CacheStore
|
||||
|
||||
class InMemoryCacheStore(
|
||||
initialData: Map<String, CacheData> = emptyMap(),
|
||||
) : CacheStore {
|
||||
val dataMap = initialData.toMutableMap()
|
||||
|
||||
override suspend fun storeData(key: String, data: CacheData) {
|
||||
dataMap[key] = data
|
||||
}
|
||||
|
||||
override suspend fun getData(key: String): CacheData? {
|
||||
return dataMap[key]
|
||||
}
|
||||
|
||||
override suspend fun deleteData(key: String) {
|
||||
dataMap.remove(key)
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +104,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
|||
implementation(project(":libraries:architecture"))
|
||||
implementation(project(":libraries:dateformatter:impl"))
|
||||
implementation(project(":libraries:di"))
|
||||
implementation(project(":libraries:cachestore:impl"))
|
||||
implementation(project(":libraries:session-storage:impl"))
|
||||
implementation(project(":libraries:mediapickers:impl"))
|
||||
implementation(project(":libraries:mediaupload:impl"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue