Add a CacheStore module.
This commit is contained in:
parent
ea1ccbf806
commit
c86532778c
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:architecture"))
|
||||||
implementation(project(":libraries:dateformatter:impl"))
|
implementation(project(":libraries:dateformatter:impl"))
|
||||||
implementation(project(":libraries:di"))
|
implementation(project(":libraries:di"))
|
||||||
|
implementation(project(":libraries:cachestore:impl"))
|
||||||
implementation(project(":libraries:session-storage:impl"))
|
implementation(project(":libraries:session-storage:impl"))
|
||||||
implementation(project(":libraries:mediapickers:impl"))
|
implementation(project(":libraries:mediapickers:impl"))
|
||||||
implementation(project(":libraries:mediaupload:impl"))
|
implementation(project(":libraries:mediaupload:impl"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue