Browse Source
* Replace SessionData DataStore with an encrypted SQLite DB. --------- Co-authored-by: Benoit Marty <benoit@matrix.org>misc/jme/add-logging-to-state-machine
Jorge Martin Espinosa
2 years ago
committed by
GitHub
38 changed files with 600 additions and 199 deletions
@ -0,0 +1,30 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
plugins { |
||||||
|
id("io.element.android-library") |
||||||
|
} |
||||||
|
|
||||||
|
android { |
||||||
|
namespace = "io.element.android.libraries.encrypteddb" |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
implementation(libs.sqldelight.driver.android) |
||||||
|
implementation(libs.sqlcipher) |
||||||
|
implementation(libs.sqlite) |
||||||
|
implementation(libs.androidx.security.crypto) |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.encrypteddb |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import com.squareup.sqldelight.android.AndroidSqliteDriver |
||||||
|
import com.squareup.sqldelight.db.SqlDriver |
||||||
|
import io.element.encrypteddb.passphrase.PassphraseProvider |
||||||
|
import net.sqlcipher.database.SupportFactory |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates an encrypted version of the [SqlDriver] using SQLCipher's [SupportFactory]. |
||||||
|
* @param passphraseProvider Provides the passphrase needed to use the SQLite database with SQLCipher. |
||||||
|
*/ |
||||||
|
class SqlCipherDriverFactory( |
||||||
|
private val passphraseProvider: PassphraseProvider, |
||||||
|
) { |
||||||
|
/** |
||||||
|
* Returns a valid [SqlDriver] with SQLCipher support. |
||||||
|
* @param schema The SQLite DB schema. |
||||||
|
* @param name The name of the database to create. |
||||||
|
* @param context Android [Context], used to instantiate the driver. |
||||||
|
*/ |
||||||
|
fun create(schema: SqlDriver.Schema, name: String, context: Context): SqlDriver { |
||||||
|
val passphrase = passphraseProvider.getPassphrase() |
||||||
|
val factory = SupportFactory(passphrase) |
||||||
|
return AndroidSqliteDriver(schema = schema, context = context, name = name, factory = factory) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.encrypteddb.passphrase |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import androidx.security.crypto.EncryptedFile |
||||||
|
import java.io.File |
||||||
|
import java.security.SecureRandom |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides a secure passphrase for SQLCipher by generating a random secret and storing it into an [EncryptedFile]. |
||||||
|
* @param context Android [Context], used by [EncryptedFile] for cryptographic operations. |
||||||
|
* @param file Destination file where the key will be stored. |
||||||
|
* @param alias Alias of the key used to encrypt & decrypt the [EncryptedFile]'s contents. |
||||||
|
* @param secretSize Length of the generated secret. |
||||||
|
*/ |
||||||
|
class RandomSecretPassphraseProvider( |
||||||
|
private val context: Context, |
||||||
|
private val file: File, |
||||||
|
private val alias: String, |
||||||
|
private val secretSize: Int = 256, |
||||||
|
) : PassphraseProvider { |
||||||
|
|
||||||
|
override fun getPassphrase(): ByteArray { |
||||||
|
val encryptedFile = EncryptedFile.Builder( |
||||||
|
file, |
||||||
|
context, |
||||||
|
alias, |
||||||
|
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB |
||||||
|
).build() |
||||||
|
return if (!file.exists()) { |
||||||
|
val secret = generateSecret() |
||||||
|
encryptedFile.openFileOutput().use { it.write(secret) } |
||||||
|
secret |
||||||
|
} else { |
||||||
|
encryptedFile.openFileInput().use { it.readBytes() } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun generateSecret(): ByteArray { |
||||||
|
val buffer = ByteArray(size = secretSize) |
||||||
|
SecureRandom().nextBytes(buffer) |
||||||
|
return buffer |
||||||
|
} |
||||||
|
} |
@ -1,109 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright (c) 2022 New Vector Ltd |
|
||||||
* |
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
* you may not use this file except in compliance with the License. |
|
||||||
* You may obtain a copy of the License at |
|
||||||
* |
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0 |
|
||||||
* |
|
||||||
* Unless required by applicable law or agreed to in writing, software |
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
* See the License for the specific language governing permissions and |
|
||||||
* limitations under the License. |
|
||||||
*/ |
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.session |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import androidx.datastore.core.DataStore |
|
||||||
import androidx.datastore.preferences.core.Preferences |
|
||||||
import androidx.datastore.preferences.core.edit |
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey |
|
||||||
import androidx.datastore.preferences.preferencesDataStore |
|
||||||
import com.squareup.anvil.annotations.ContributesBinding |
|
||||||
import io.element.android.libraries.di.AppScope |
|
||||||
import io.element.android.libraries.di.ApplicationContext |
|
||||||
import io.element.android.libraries.di.SingleIn |
|
||||||
import io.element.android.libraries.matrix.core.SessionId |
|
||||||
import kotlinx.coroutines.flow.Flow |
|
||||||
import kotlinx.coroutines.flow.firstOrNull |
|
||||||
import kotlinx.coroutines.flow.map |
|
||||||
import kotlinx.serialization.Serializable |
|
||||||
import kotlinx.serialization.decodeFromString |
|
||||||
import kotlinx.serialization.encodeToString |
|
||||||
import kotlinx.serialization.json.Json |
|
||||||
import org.matrix.rustcomponents.sdk.Session |
|
||||||
import javax.inject.Inject |
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_sessions") |
|
||||||
|
|
||||||
// TODO It contains the access token, so it has to be stored in a more secured storage. |
|
||||||
private val sessionKey = stringPreferencesKey("session") |
|
||||||
|
|
||||||
@SingleIn(AppScope::class) |
|
||||||
@ContributesBinding(AppScope::class) |
|
||||||
class PreferencesSessionStore @Inject constructor( |
|
||||||
@ApplicationContext context: Context |
|
||||||
) : SessionStore { |
|
||||||
@Serializable |
|
||||||
data class SessionData( |
|
||||||
val accessToken: String, |
|
||||||
val deviceId: String, |
|
||||||
val homeserverUrl: String, |
|
||||||
val isSoftLogout: Boolean, |
|
||||||
val refreshToken: String?, |
|
||||||
val userId: String, |
|
||||||
val slidingSyncProxy: String? |
|
||||||
) |
|
||||||
|
|
||||||
private val store = context.dataStore |
|
||||||
|
|
||||||
override fun isLoggedIn(): Flow<Boolean> { |
|
||||||
return store.data.map { prefs -> |
|
||||||
prefs[sessionKey] != null |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override suspend fun storeData(session: Session) { |
|
||||||
store.edit { prefs -> |
|
||||||
val sessionData = SessionData( |
|
||||||
accessToken = session.accessToken, |
|
||||||
deviceId = session.deviceId, |
|
||||||
homeserverUrl = session.homeserverUrl, |
|
||||||
isSoftLogout = session.isSoftLogout, |
|
||||||
refreshToken = session.refreshToken, |
|
||||||
userId = session.userId, |
|
||||||
slidingSyncProxy = session.slidingSyncProxy |
|
||||||
) |
|
||||||
val encodedSession = Json.encodeToString(sessionData) |
|
||||||
prefs[sessionKey] = encodedSession |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override suspend fun getLatestSession(): Session? { |
|
||||||
return store.data.firstOrNull()?.let { prefs -> |
|
||||||
val encodedSession = prefs[sessionKey] ?: return@let null |
|
||||||
val sessionData = Json.decodeFromString<SessionData>(encodedSession) |
|
||||||
Session( |
|
||||||
accessToken = sessionData.accessToken, |
|
||||||
deviceId = sessionData.deviceId, |
|
||||||
homeserverUrl = sessionData.homeserverUrl, |
|
||||||
isSoftLogout = sessionData.isSoftLogout, |
|
||||||
refreshToken = sessionData.refreshToken, |
|
||||||
userId = sessionData.userId, |
|
||||||
slidingSyncProxy = sessionData.slidingSyncProxy |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override suspend fun getSession(sessionId: SessionId): Session? { |
|
||||||
//TODO we should have a proper session management |
|
||||||
return getLatestSession() |
|
||||||
} |
|
||||||
|
|
||||||
override suspend fun reset() { |
|
||||||
store.edit { it.clear() } |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,51 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
plugins { |
||||||
|
id("io.element.android-library") |
||||||
|
alias(libs.plugins.anvil) |
||||||
|
alias(libs.plugins.sqldelight) |
||||||
|
} |
||||||
|
|
||||||
|
android { |
||||||
|
namespace = "io.element.android.libraries.sessionstorage" |
||||||
|
} |
||||||
|
|
||||||
|
anvil { |
||||||
|
generateDaggerFactories.set(true) |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
implementation(libs.dagger) |
||||||
|
implementation(projects.libraries.core) |
||||||
|
implementation(projects.libraries.encryptedDb) |
||||||
|
implementation(libs.sqldelight.driver.android) |
||||||
|
implementation(libs.sqlcipher) |
||||||
|
implementation(libs.sqlite) |
||||||
|
implementation(libs.androidx.security.crypto) |
||||||
|
implementation(projects.libraries.di) |
||||||
|
implementation(libs.sqldelight.coroutines) |
||||||
|
|
||||||
|
testImplementation(libs.test.junit) |
||||||
|
testImplementation(libs.test.truth) |
||||||
|
testImplementation(libs.test.turbine) |
||||||
|
testImplementation(libs.coroutines.test) |
||||||
|
testImplementation(libs.sqldelight.driver.jvm) |
||||||
|
} |
||||||
|
|
||||||
|
sqldelight { |
||||||
|
database("SessionDatabase") {} |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.sessionstorage |
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding |
||||||
|
import com.squareup.sqldelight.runtime.coroutines.asFlow |
||||||
|
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull |
||||||
|
import io.element.android.libraries.di.AppScope |
||||||
|
import io.element.android.libraries.di.SingleIn |
||||||
|
import io.element.android.libraries.matrix.session.SessionData |
||||||
|
import kotlinx.coroutines.flow.Flow |
||||||
|
import kotlinx.coroutines.flow.map |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
@SingleIn(AppScope::class) |
||||||
|
@ContributesBinding(AppScope::class) |
||||||
|
class DatabaseSessionStore @Inject constructor( |
||||||
|
private val database: SessionDatabase, |
||||||
|
) : SessionStore { |
||||||
|
|
||||||
|
override fun isLoggedIn(): Flow<Boolean> { |
||||||
|
return database.sessionDataQueries.selectFirst().asFlow().mapToOneOrNull().map { it != null } |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun storeData(sessionData: SessionData) { |
||||||
|
database.sessionDataQueries.insertSessionData(sessionData) |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun getLatestSession(): SessionData? { |
||||||
|
return database.sessionDataQueries.selectFirst() |
||||||
|
.executeAsOneOrNull() |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun getSession(sessionId: String): SessionData? { |
||||||
|
return database.sessionDataQueries.selectByUserId(sessionId) |
||||||
|
.executeAsOneOrNull() |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun removeSession(sessionId: String) { |
||||||
|
database.sessionDataQueries.removeSession(sessionId) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.sessionstorage.di |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import com.squareup.anvil.annotations.ContributesTo |
||||||
|
import dagger.Module |
||||||
|
import dagger.Provides |
||||||
|
import io.element.android.libraries.di.AppScope |
||||||
|
import io.element.android.libraries.di.ApplicationContext |
||||||
|
import io.element.android.libraries.di.SingleIn |
||||||
|
import io.element.android.libraries.sessionstorage.SessionDatabase |
||||||
|
import io.element.encrypteddb.SqlCipherDriverFactory |
||||||
|
import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider |
||||||
|
|
||||||
|
@Module |
||||||
|
@ContributesTo(AppScope::class) |
||||||
|
object SessionStorageModule { |
||||||
|
@Provides |
||||||
|
@SingleIn(AppScope::class) |
||||||
|
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase { |
||||||
|
val name = "session_database" |
||||||
|
val secretFile = context.getDatabasePath("$name.key") |
||||||
|
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile, name) |
||||||
|
val driver = SqlCipherDriverFactory(passphraseProvider) |
||||||
|
.create(SessionDatabase.Schema, "$name.db", context) |
||||||
|
return SessionDatabase(driver) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
CREATE TABLE SessionData ( |
||||||
|
userId TEXT NOT NULL PRIMARY KEY, |
||||||
|
deviceId TEXT NOT NULL, |
||||||
|
accessToken TEXT NOT NULL, |
||||||
|
refreshToken TEXT, |
||||||
|
homeserverUrl TEXT NOT NULL, |
||||||
|
isSoftLogout INTEGER AS Boolean NOT NULL DEFAULT 0, |
||||||
|
slidingSyncProxy TEXT |
||||||
|
); |
||||||
|
|
||||||
|
selectFirst: |
||||||
|
SELECT * FROM SessionData LIMIT 1; |
||||||
|
|
||||||
|
selectByUserId: |
||||||
|
SELECT * FROM SessionData WHERE userId = ?; |
||||||
|
|
||||||
|
insertSessionData: |
||||||
|
INSERT INTO SessionData(userId, deviceId, accessToken, refreshToken, homeserverUrl, isSoftLogout, slidingSyncProxy) VALUES ?; |
||||||
|
|
||||||
|
removeSession: |
||||||
|
DELETE FROM SessionData WHERE userId = ?; |
@ -0,0 +1,112 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.sessionstorage |
||||||
|
|
||||||
|
import app.cash.turbine.test |
||||||
|
import com.google.common.truth.Truth.assertThat |
||||||
|
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver |
||||||
|
import io.element.android.libraries.matrix.session.SessionData |
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||||
|
import kotlinx.coroutines.test.runTest |
||||||
|
import org.junit.Before |
||||||
|
import org.junit.Test |
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class) |
||||||
|
class DatabaseSessionStoreTests { |
||||||
|
|
||||||
|
private lateinit var database: SessionDatabase |
||||||
|
private lateinit var databaseSessionStore: DatabaseSessionStore |
||||||
|
|
||||||
|
private val aSessionData = SessionData( |
||||||
|
userId = "userId", |
||||||
|
deviceId = "deviceId", |
||||||
|
accessToken = "accessToken", |
||||||
|
refreshToken = "refreshToken", |
||||||
|
homeserverUrl = "homeserverUrl", |
||||||
|
isSoftLogout = false, |
||||||
|
slidingSyncProxy = null |
||||||
|
) |
||||||
|
|
||||||
|
@Before |
||||||
|
fun setup() { |
||||||
|
// Initialise in memory SQLite driver |
||||||
|
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) |
||||||
|
SessionDatabase.Schema.create(driver) |
||||||
|
|
||||||
|
database = SessionDatabase(driver) |
||||||
|
databaseSessionStore = DatabaseSessionStore(database) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `storeData persists the SessionData into the DB`() = runTest { |
||||||
|
assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isNull() |
||||||
|
|
||||||
|
databaseSessionStore.storeData(aSessionData) |
||||||
|
|
||||||
|
assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isEqualTo(aSessionData) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `isLoggedIn emits true while there are sessions in the DB`() = runTest { |
||||||
|
databaseSessionStore.isLoggedIn().test { |
||||||
|
assertThat(awaitItem()).isFalse() |
||||||
|
database.sessionDataQueries.insertSessionData(aSessionData) |
||||||
|
assertThat(awaitItem()).isTrue() |
||||||
|
database.sessionDataQueries.removeSession(aSessionData.userId) |
||||||
|
assertThat(awaitItem()).isFalse() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `getLatestSession gets the first session in the DB`() = runTest { |
||||||
|
database.sessionDataQueries.insertSessionData(aSessionData) |
||||||
|
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId")) |
||||||
|
|
||||||
|
val latestSession = databaseSessionStore.getLatestSession() |
||||||
|
|
||||||
|
assertThat(latestSession).isEqualTo(aSessionData) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `getSession returns a matching session in DB if exists`() = runTest { |
||||||
|
database.sessionDataQueries.insertSessionData(aSessionData) |
||||||
|
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId")) |
||||||
|
|
||||||
|
val foundSession = databaseSessionStore.getSession(aSessionData.userId) |
||||||
|
|
||||||
|
assertThat(foundSession).isEqualTo(aSessionData) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `getSession returns null if a no matching session exists in DB`() = runTest { |
||||||
|
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId")) |
||||||
|
|
||||||
|
val foundSession = databaseSessionStore.getSession(aSessionData.userId) |
||||||
|
|
||||||
|
assertThat(foundSession).isNull() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `removeSession removes the associated session in DB`() = runTest { |
||||||
|
database.sessionDataQueries.insertSessionData(aSessionData) |
||||||
|
|
||||||
|
databaseSessionStore.removeSession(aSessionData.userId) |
||||||
|
|
||||||
|
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull() |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.samples.minimal |
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.session.SessionData |
||||||
|
import io.element.android.libraries.sessionstorage.SessionStore |
||||||
|
import kotlinx.coroutines.flow.Flow |
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow |
||||||
|
import kotlinx.coroutines.flow.map |
||||||
|
|
||||||
|
class InMemorySessionStore : SessionStore { |
||||||
|
|
||||||
|
private var sessionData = MutableStateFlow<SessionData?>(null) |
||||||
|
|
||||||
|
override fun isLoggedIn(): Flow<Boolean> { |
||||||
|
return sessionData.map { it != null } |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun storeData(session: SessionData) { |
||||||
|
sessionData.value = session |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun getSession(sessionId: String): SessionData? { |
||||||
|
return sessionData.value.takeIf { it?.userId == sessionId } |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun getLatestSession(): SessionData? { |
||||||
|
return sessionData.value |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun removeSession(sessionId: String) { |
||||||
|
if (sessionData.value?.userId == sessionId) { |
||||||
|
sessionData.value = null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<!-- |
||||||
|
~ Copyright (c) 2023 New Vector Ltd |
||||||
|
~ |
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
~ you may not use this file except in compliance with the License. |
||||||
|
~ You may obtain a copy of the License at |
||||||
|
~ |
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
~ |
||||||
|
~ Unless required by applicable law or agreed to in writing, software |
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
~ See the License for the specific language governing permissions and |
||||||
|
~ limitations under the License. |
||||||
|
--> |
||||||
|
|
||||||
|
<resources> |
||||||
|
<style name="Theme.ElementX" parent="android:Theme.Material.NoActionBar" /> |
||||||
|
</resources> |
Loading…
Reference in new issue