First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.api"
}
dependencies {
implementation(libs.coroutines.core)
implementation(projects.libraries.matrix.api)
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.api
import kotlinx.coroutines.flow.Flow
/**
* Store data related to push about a user.
*/
interface UserPushStore {
suspend fun getPushProviderName(): String?
suspend fun setPushProviderName(value: String)
suspend fun getCurrentRegisteredPushKey(): String?
suspend fun setCurrentRegisteredPushKey(value: String?)
fun getNotificationEnabledForDevice(): Flow<Boolean>
suspend fun setNotificationEnabledForDevice(enabled: Boolean)
fun ignoreRegistrationError(): Flow<Boolean>
suspend fun setIgnoreRegistrationError(ignore: Boolean)
/**
* Return true if Pin code is disabled, or if user set the settings to see full notification content.
*/
fun useCompleteNotificationFormat(): Boolean
suspend fun reset()
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.api
import io.element.android.libraries.matrix.api.core.SessionId
/**
* Store data related to push about a user.
*/
interface UserPushStoreFactory {
fun getOrCreate(userId: SessionId): UserPushStore
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.api.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
interface PushClientSecret {
/**
* To call when registering a pusher. It will return the existing secret or create a new one.
*/
suspend fun getSecretForUser(userId: SessionId): String
/**
* To call when receiving a push containing a client secret.
* Return null if not found.
*/
suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.api.clientsecret
interface PushClientSecretFactory {
fun create(): String
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.api.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
interface PushClientSecretStore {
suspend fun storeSecret(userId: SessionId, clientSecret: String)
suspend fun getSecret(userId: SessionId): String?
suspend fun resetSecret(userId: SessionId)
suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
}

View File

@@ -0,0 +1,46 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector 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.push.pushstore.impl"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
}
setupDependencyInjection()
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.pushstore.api)
implementation(libs.androidx.corektx)
implementation(libs.androidx.datastore.preferences)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.pushstore.test)
androidTestImplementation(libs.coroutines.test)
androidTestImplementation(libs.test.core)
androidTestImplementation(libs.test.junit)
androidTestImplementation(libs.test.truth)
androidTestImplementation(libs.test.runner)
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.impl
import androidx.test.platform.app.InstrumentationRegistry
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.UserPushStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Test
import kotlin.concurrent.thread
/**
* Note: to clear the emulator, invoke:
* adb uninstall io.element.android.libraries.push.pushstore.impl.test
*/
class DefaultUserPushStoreFactoryTest {
/**
* Ensure that creating UserPushStore is thread safe.
*/
@Test
fun testParallelCreation() {
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
val sessionId = SessionId("@alice:server.org")
val userPushStoreFactory = DefaultUserPushStoreFactory(context)
var userPushStore1: UserPushStore? = null
val thread1 = thread {
userPushStore1 = userPushStoreFactory.getOrCreate(sessionId)
}
var userPushStore2: UserPushStore? = null
val thread2 = thread {
userPushStore2 = userPushStoreFactory.getOrCreate(sessionId)
}
thread1.join()
thread2.join()
runBlocking {
userPushStore1!!.getNotificationEnabledForDevice().first()
userPushStore2!!.getNotificationEnabledForDevice().first()
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.impl
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import java.util.concurrent.ConcurrentHashMap
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultUserPushStoreFactory(
@ApplicationContext private val context: Context,
private val preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : UserPushStoreFactory {
// We can have only one class accessing a single data store, so keep a cache of them.
private val cache = ConcurrentHashMap<SessionId, UserPushStore>()
override fun getOrCreate(userId: SessionId): UserPushStore {
return cache.getOrPut(userId) {
UserPushStoreDataStore(
context = context,
userId = userId,
factory = preferenceDataStoreFactory,
)
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.impl
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import io.element.android.libraries.pushstore.api.UserPushStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import timber.log.Timber
/**
* Store data related to push about a user.
*/
class UserPushStoreDataStore(
private val context: Context,
userId: SessionId,
factory: PreferenceDataStoreFactory,
) : UserPushStore {
// Hash the sessionId to get rid of exotic chars and take only the first 16 chars.
// The risk of collision is not high.
private val preferenceName = "push_store_${userId.value.hash().take(16)}"
init {
// Migrate legacy data. Previous file can be too long if the userId is too long. The userId can be up to 255 chars.
// Example of long file path, with `averylonguserid` replacing a very longer name
// /data/user/0/io.element.android.x.debug/files/datastore/push_store_@averylonguserid:example.org.preferences_pb
val legacyFile = context.preferencesDataStoreFile("push_store_$userId")
if (legacyFile.exists()) {
Timber.d("Migrating legacy push data store for $userId")
if (!legacyFile.renameTo(context.preferencesDataStoreFile(preferenceName))) {
Timber.w("Failed to migrate legacy push data store for $userId")
}
}
}
private val store: DataStore<Preferences> = factory.create(preferenceName)
private val pushProviderName = stringPreferencesKey("pushProviderName")
private val currentPushKey = stringPreferencesKey("currentPushKey")
private val notificationEnabled = booleanPreferencesKey("notificationEnabled")
private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError")
override suspend fun getPushProviderName(): String? {
return store.data.first()[pushProviderName]
}
override suspend fun setPushProviderName(value: String) {
store.edit {
it[pushProviderName] = value
}
}
override suspend fun getCurrentRegisteredPushKey(): String? {
return store.data.first()[currentPushKey]
}
override suspend fun setCurrentRegisteredPushKey(value: String?) {
store.edit {
if (value == null) {
it.remove(currentPushKey)
} else {
it[currentPushKey] = value
}
}
}
override fun getNotificationEnabledForDevice(): Flow<Boolean> {
return store.data.map { it[notificationEnabled].orTrue() }
}
override suspend fun setNotificationEnabledForDevice(enabled: Boolean) {
store.edit {
it[notificationEnabled] = enabled
}
}
override fun useCompleteNotificationFormat(): Boolean {
return true
}
override fun ignoreRegistrationError(): Flow<Boolean> {
return store.data.map { it[ignoreRegistrationError].orFalse() }
}
override suspend fun setIgnoreRegistrationError(ignore: Boolean) {
store.edit {
it[ignoreRegistrationError] = ignore
}
}
override suspend fun reset() {
store.edit {
it.clear()
}
// Also delete the file
context.preferencesDataStoreFile(preferenceName).delete()
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.impl.clientsecret
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
import kotlinx.coroutines.flow.first
@ContributesBinding(AppScope::class)
class DataStorePushClientSecretStore(
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : PushClientSecretStore {
private val dataStore = preferenceDataStoreFactory.create("push_client_secret_store")
override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
dataStore.edit { settings ->
settings[getPreferenceKeyForUser(userId)] = clientSecret
}
}
override suspend fun getSecret(userId: SessionId): String? {
return dataStore.data.first()[getPreferenceKeyForUser(userId)]
}
override suspend fun resetSecret(userId: SessionId) {
dataStore.edit { settings ->
settings.remove(getPreferenceKeyForUser(userId))
}
}
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
val keyValues = dataStore.data.first().asMap()
val matchingKey = keyValues.keys.find {
keyValues[it] == clientSecret
}
return matchingKey?.name?.let(::SessionId)
}
private fun getPreferenceKeyForUser(userId: SessionId) = stringPreferencesKey(userId.value)
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.impl.clientsecret
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
@ContributesBinding(AppScope::class)
class DefaultPushClientSecret(
private val pushClientSecretFactory: PushClientSecretFactory,
private val pushClientSecretStore: PushClientSecretStore,
) : PushClientSecret {
override suspend fun getSecretForUser(userId: SessionId): String {
val existingSecret = pushClientSecretStore.getSecret(userId)
if (existingSecret != null) {
return existingSecret
}
val newSecret = pushClientSecretFactory.create()
pushClientSecretStore.storeSecret(userId, newSecret)
return newSecret
}
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
return pushClientSecretStore.getUserIdFromSecret(clientSecret)
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.impl.clientsecret
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory
import java.util.UUID
@ContributesBinding(AppScope::class)
class DefaultPushClientSecretFactory : PushClientSecretFactory {
override fun create(): String {
return UUID.randomUUID().toString()
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector 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.pushstore.impl
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class UserPushStoreDataStoreTest {
@Test
fun `test getPushProviderName`() = runTest {
val sut = createUserPushStoreDataStore()
assertThat(sut.getPushProviderName()).isNull()
sut.setPushProviderName("name")
assertThat(sut.getPushProviderName()).isEqualTo("name")
}
@Test
fun `test getCurrentRegisteredPushKey`() = runTest {
val sut = createUserPushStoreDataStore()
assertThat(sut.getCurrentRegisteredPushKey()).isNull()
sut.setCurrentRegisteredPushKey("aKey")
assertThat(sut.getCurrentRegisteredPushKey()).isEqualTo("aKey")
sut.setCurrentRegisteredPushKey(null)
assertThat(sut.getCurrentRegisteredPushKey()).isNull()
}
@Test
fun `test getNotificationEnabledForDevice`() = runTest {
val sut = createUserPushStoreDataStore()
assertThat(sut.getNotificationEnabledForDevice().first()).isTrue()
sut.setNotificationEnabledForDevice(false)
assertThat(sut.getNotificationEnabledForDevice().first()).isFalse()
sut.setNotificationEnabledForDevice(true)
assertThat(sut.getNotificationEnabledForDevice().first()).isTrue()
}
@Test
fun `test useCompleteNotificationFormat`() = runTest {
val sut = createUserPushStoreDataStore()
assertThat(sut.useCompleteNotificationFormat()).isTrue()
}
@Test
fun `test ignoreRegistrationError`() = runTest {
val sut = createUserPushStoreDataStore()
assertThat(sut.ignoreRegistrationError().first()).isFalse()
sut.setIgnoreRegistrationError(true)
assertThat(sut.ignoreRegistrationError().first()).isTrue()
sut.setIgnoreRegistrationError(false)
assertThat(sut.ignoreRegistrationError().first()).isFalse()
}
@Test
fun `test reset`() = runTest {
val sut = createUserPushStoreDataStore()
sut.setPushProviderName("name")
sut.setCurrentRegisteredPushKey("aKey")
sut.setNotificationEnabledForDevice(false)
sut.setIgnoreRegistrationError(true)
sut.reset()
assertThat(sut.getPushProviderName()).isNull()
assertThat(sut.getCurrentRegisteredPushKey()).isNull()
assertThat(sut.getNotificationEnabledForDevice().first()).isTrue()
assertThat(sut.ignoreRegistrationError().first()).isFalse()
}
@Test
fun `ensure a store is created per session`() = runTest {
val sut1 = createUserPushStoreDataStore()
sut1.setPushProviderName("name")
val sut2 = createUserPushStoreDataStore(A_SESSION_ID_2)
assertThat(sut1.getPushProviderName()).isEqualTo("name")
assertThat(sut2.getPushProviderName()).isNull()
}
private fun createUserPushStoreDataStore(
sessionId: SessionId = A_SESSION_ID,
) = UserPushStoreDataStore(
context = InstrumentationRegistry.getInstrumentation().context,
userId = sessionId,
factory = FakePreferenceDataStoreFactory(),
)
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.impl.clientsecret
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.InMemoryPushClientSecretStore
import kotlinx.coroutines.test.runTest
import org.junit.Test
private val A_USER_ID_0 = SessionId("@A_USER_ID_0:domain")
private val A_USER_ID_1 = SessionId("@A_USER_ID_1:domain")
private const val A_UNKNOWN_SECRET = "A_UNKNOWN_SECRET"
internal class DefaultPushClientSecretTest {
@Test
fun test() = runTest {
val factory = FakePushClientSecretFactory()
val store = InMemoryPushClientSecretStore()
val sut = DefaultPushClientSecret(factory, store)
val secret0 = factory.getSecretForUser(0)
val secret1 = factory.getSecretForUser(1)
assertThat(store.getSecrets()).isEmpty()
assertThat(sut.getUserIdFromSecret(secret0)).isNull()
// Create a secret
assertThat(sut.getSecretForUser(A_USER_ID_0)).isEqualTo(secret0)
assertThat(store.getSecrets()).hasSize(1)
// Same secret returned
assertThat(sut.getSecretForUser(A_USER_ID_0)).isEqualTo(secret0)
assertThat(store.getSecrets()).hasSize(1)
// Another secret returned for another user
assertThat(sut.getSecretForUser(A_USER_ID_1)).isEqualTo(secret1)
assertThat(store.getSecrets()).hasSize(2)
// Get users from secrets
assertThat(sut.getUserIdFromSecret(secret0)).isEqualTo(A_USER_ID_0)
assertThat(sut.getUserIdFromSecret(secret1)).isEqualTo(A_USER_ID_1)
// Unknown secret
assertThat(sut.getUserIdFromSecret(A_UNKNOWN_SECRET)).isNull()
// Check the store content
assertThat(store.getSecrets()).isEqualTo(
mapOf(
A_USER_ID_0 to secret0,
A_USER_ID_1 to secret1,
)
)
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.impl.clientsecret
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory
private const val A_SECRET_PREFIX = "A_SECRET_"
class FakePushClientSecretFactory : PushClientSecretFactory {
private var index = 0
override fun create() = getSecretForUser(index++)
fun getSecretForUser(i: Int): String {
return A_SECRET_PREFIX + i
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.test"
}
dependencies {
api(projects.libraries.matrix.api)
api(libs.coroutines.core)
implementation(libs.coroutines.test)
implementation(projects.tests.testutils)
implementation(projects.libraries.pushstore.api)
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.test.userpushstore
import io.element.android.libraries.pushstore.api.UserPushStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeUserPushStore(
private var pushProviderName: String? = null
) : UserPushStore {
private var currentRegisteredPushKey: String? = null
private val notificationEnabledForDevice = MutableStateFlow(true)
private val ignoreRegistrationError = MutableStateFlow(false)
override suspend fun getPushProviderName(): String? {
return pushProviderName
}
override suspend fun setPushProviderName(value: String) {
pushProviderName = value
}
override suspend fun getCurrentRegisteredPushKey(): String? {
return currentRegisteredPushKey
}
override suspend fun setCurrentRegisteredPushKey(value: String?) {
currentRegisteredPushKey = value
}
override fun getNotificationEnabledForDevice(): Flow<Boolean> {
return notificationEnabledForDevice
}
override suspend fun setNotificationEnabledForDevice(enabled: Boolean) {
notificationEnabledForDevice.value = enabled
}
override fun useCompleteNotificationFormat(): Boolean {
return true
}
override fun ignoreRegistrationError(): Flow<Boolean> {
return ignoreRegistrationError
}
override suspend fun setIgnoreRegistrationError(ignore: Boolean) {
ignoreRegistrationError.value = ignore
}
override suspend fun reset() {
pushProviderName = null
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.test.userpushstore
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
class FakeUserPushStoreFactory(
val userPushStore: (SessionId) -> UserPushStore = { FakeUserPushStore() }
) : UserPushStoreFactory {
override fun getOrCreate(userId: SessionId): UserPushStore {
return userPushStore(userId)
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector 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.pushstore.test.userpushstore.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import io.element.android.tests.testutils.lambda.lambdaError
class FakePushClientSecret(
private val getSecretForUserResult: (SessionId) -> String = { lambdaError() },
private val getUserIdFromSecretResult: (String) -> SessionId? = { lambdaError() }
) : PushClientSecret {
override suspend fun getSecretForUser(userId: SessionId): String {
return getSecretForUserResult(userId)
}
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
return getUserIdFromSecretResult(clientSecret)
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.pushstore.test.userpushstore.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
class InMemoryPushClientSecretStore : PushClientSecretStore {
private val secrets = mutableMapOf<SessionId, String>()
fun getSecrets(): Map<SessionId, String> = secrets
override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
secrets[userId] = clientSecret
}
override suspend fun getSecret(userId: SessionId): String? {
return secrets[userId]
}
override suspend fun resetSecret(userId: SessionId) {
secrets.remove(userId)
}
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
return secrets.keys.firstOrNull { secrets[it] == clientSecret }
}
}