forked from dsutanto/bChot-android
First Commit
This commit is contained in:
19
libraries/pushstore/api/build.gradle.kts
Normal file
19
libraries/pushstore/api/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
46
libraries/pushstore/impl/build.gradle.kts
Normal file
46
libraries/pushstore/impl/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
23
libraries/pushstore/test/build.gradle.kts
Normal file
23
libraries/pushstore/test/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user