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 @@
@@ -0,0 +1 @@
|
||||
Store session data in a secure storage. |
@ -0,0 +1,30 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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