1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3634 Notifications | Store pushKeyUpdates as Map (#2381)

This commit is contained in:
Konstantin Ivanov 2025-05-07 12:06:44 +02:00 committed by GitHub
parent 1ef93d72db
commit 11fd1abeb2
Signed by: github
GPG key ID: B5690EEEBB952194
6 changed files with 128 additions and 59 deletions

View file

@ -40,8 +40,8 @@ object EmojiModule {
@JvmStatic
@Provides
@Singleton
fun provideEmojiSuggestStorage(context: Context): EmojiSuggestStorage {
return DefaultEmojiSuggestStorage(context, Gson())
fun provideEmojiSuggestStorage(context: Context, gson: Gson): EmojiSuggestStorage {
return DefaultEmojiSuggestStorage(context, gson)
}
@JvmStatic

View file

@ -19,6 +19,7 @@ import com.anytypeio.anytype.middleware.interactor.events.PushKeyMiddlewareChann
import com.anytypeio.anytype.presentation.notifications.NotificationsProvider
import com.anytypeio.anytype.presentation.notifications.PushKeyProvider
import com.anytypeio.anytype.presentation.notifications.PushKeyProviderImpl
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import javax.inject.Named
@ -75,13 +76,15 @@ object NotificationsModule {
@Named("encrypted") sharedPreferences: SharedPreferences,
dispatchers: AppCoroutineDispatchers,
@Named(ConfigModule.DEFAULT_APP_COROUTINE_SCOPE) scope: CoroutineScope,
channel: PushKeyChannel
channel: PushKeyChannel,
gson: Gson
): PushKeyProvider {
return PushKeyProviderImpl(
sharedPreferences = sharedPreferences,
dispatchers = dispatchers,
scope = scope,
channel = channel
channel = channel,
gson = gson
)
}

View file

@ -36,6 +36,7 @@ import com.anytypeio.anytype.other.DefaultDebugConfig
import com.anytypeio.anytype.presentation.util.StringResourceProviderImpl
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProviderImpl
import com.google.gson.Gson
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -105,6 +106,11 @@ object UtilModule {
fun provideStringResourceProvider(context: Context): StringResourceProvider =
StringResourceProviderImpl(context)
@JvmStatic
@Provides
@Singleton
fun provideGson(): Gson = Gson()
@Module
interface Bindings {

View file

@ -50,6 +50,8 @@ dependencies {
compileOnly libs.javaxInject
implementation libs.gson
testImplementation libs.junit
testImplementation libs.kotlinTest
testImplementation libs.mockitoKotlin

View file

@ -3,27 +3,30 @@ package com.anytypeio.anytype.presentation.notifications
import android.content.SharedPreferences
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.chats.PushKeyChannel
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
interface PushKeyProvider {
fun getPushKey(): PushKey
fun getPushKey(): Map<String, PushKey>
}
data class PushKey(
val key: String,
val id: String
val id: String,
val value: String
) {
companion object {
val EMPTY = PushKey(key = "", id = "")
val EMPTY = PushKey(value = "", id = "")
}
}
class PushKeyProviderImpl @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val channel: PushKeyChannel,
private val gson: Gson,
dispatchers: AppCoroutineDispatchers,
scope: CoroutineScope
) : PushKeyProvider {
@ -36,30 +39,37 @@ class PushKeyProviderImpl @Inject constructor(
.collect { event ->
Timber.d("New push key updates: $event")
savePushKey(
pushKey = event.encryptionKey,
pushKeyId = event.encryptionKeyId
id = event.encryptionKeyId,
value = event.encryptionKey
)
}
}
}
private fun savePushKey(pushKey: String?, pushKeyId: String?) {
private fun savePushKey(id: String?, value: String?) {
val storedKeys = getStoredPushKeys().toMutableMap()
if (!value.isNullOrEmpty() && !id.isNullOrEmpty()) {
storedKeys[id] = PushKey(id = id, value = value)
}
sharedPreferences.edit().apply {
putString(PREF_PUSH_KEY, pushKey)
putString(PREF_PUSH_KEY_ID, pushKeyId)
putString(PREF_PUSH_KEYS, gson.toJson(storedKeys))
apply()
}
}
override fun getPushKey(): PushKey {
val pushKey = sharedPreferences.getString(PREF_PUSH_KEY, "") ?: ""
val pushKeyId = sharedPreferences.getString(PREF_PUSH_KEY_ID, "") ?: ""
Timber.d("PushKeyProvider getPushKey: $pushKey, $pushKeyId")
return PushKey(key = pushKey, id = pushKeyId)
override fun getPushKey(): Map<String, PushKey> {
val storedKeysJson = sharedPreferences.getString(PREF_PUSH_KEYS, "{}") ?: "{}"
return gson.fromJson(storedKeysJson, object : TypeToken<Map<String, PushKey>>() {}.type)
}
private fun getStoredPushKeys(): Map<String, PushKey> {
val storedKeysJson = sharedPreferences.getString(PREF_PUSH_KEYS, "{}") ?: "{}"
return gson.fromJson(storedKeysJson, object : TypeToken<Map<String, PushKey>>() {}.type)
}
companion object {
const val PREF_PUSH_KEY = "pref.push_key"
const val PREF_PUSH_KEY_ID = "pref.push_key_id"
const val PREF_PUSH_KEYS = "pref.push_keys"
}
}

View file

@ -7,6 +7,8 @@ import androidx.test.core.app.ApplicationProvider
import com.anytypeio.anytype.core_models.chats.PushKeyUpdate
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.chats.PushKeyChannel
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
@ -34,6 +36,8 @@ class PushKeyProviderImplTest {
@Mock
private lateinit var mockChannel: PushKeyChannel
private val gson = Gson()
private lateinit var sharedPreferences: SharedPreferences
private val dispatcher = StandardTestDispatcher(name = "Default test dispatcher")
private val testScope = TestScope(dispatcher)
@ -62,14 +66,14 @@ class PushKeyProviderImplTest {
fun `getPushKey should return empty PushKey when no key is set`() = runTest {
val pushKeyProvider = createPushKeyProvider()
val pushKey = pushKeyProvider.getPushKey()
assertTrue(pushKey == PushKey.EMPTY)
assertTrue(pushKey.isEmpty())
}
@Test
fun `savePushKey should store the key and keyId in shared preferences when channel emits`() =
runTest(dispatcher) {
val pushKey = "test_push_key"
val pushKeyId = "test_push_key_id"
val pushKey = "test_push_key"
// Simulate the event emission from the channel
val channelFlow = MutableSharedFlow<PushKeyUpdate>(replay = 0)
@ -85,30 +89,18 @@ class PushKeyProviderImplTest {
dispatcher.scheduler.advanceUntilIdle() // Allow the processing coroutine to run
// Verify the stored values in SharedPreferences
val storedKey = sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEY, null)
val storedKeyId =
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEY_ID, null)
val storedKeysJson =
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEYS, "{}") ?: "{}"
val storedKeys: Map<String, PushKey> = gson.fromJson(
storedKeysJson,
object : TypeToken<Map<String, PushKey>>() {}.type
)
assertEquals(pushKey, storedKey)
assertEquals(pushKeyId, storedKeyId)
assertTrue(storedKeys.containsKey(pushKeyId))
assertEquals(pushKeyId, storedKeys[pushKeyId]?.id)
assertEquals(pushKey, storedKeys[pushKeyId]?.value)
}
@Test
fun `getPushKey should return the stored key when a key is set`() = runTest {
val pushKey = PushKey(key = "test_push_key", id = "test_push_key_id")
sharedPreferences.edit()
.putString(PushKeyProviderImpl.PREF_PUSH_KEY_ID, pushKey.id)
.apply()
sharedPreferences.edit()
.putString(PushKeyProviderImpl.PREF_PUSH_KEY, pushKey.key)
.apply()
val pushKeyProvider = createPushKeyProvider()
val retrievedKey = pushKeyProvider.getPushKey()
assertEquals(pushKey, retrievedKey)
}
@Test
fun `observation should update shared preferences on subsequent channel emissions`() =
runTest(dispatcher) {
@ -136,14 +128,13 @@ class PushKeyProviderImplTest {
dispatcher.scheduler.advanceUntilIdle() // Process emission
// Verify initial storage
assertEquals(
initialKey,
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEY, null)
)
assertEquals(
initialKeyId,
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEY_ID, null)
)
val storedKeysJson =
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEYS, "{}") ?: "{}"
val storedKeys: Map<String, PushKey> =
gson.fromJson(storedKeysJson, object : TypeToken<Map<String, PushKey>>() {}.type)
assertTrue(storedKeys.containsKey(initialKeyId))
assertEquals(initialKeyId, storedKeys[initialKeyId]?.id)
assertEquals(initialKey, storedKeys[initialKeyId]?.value)
// Emit updated event
channelFlow.emit(
@ -155,16 +146,72 @@ class PushKeyProviderImplTest {
dispatcher.scheduler.advanceUntilIdle() // Process emission
// Verify updated storage
assertEquals(
updatedKey,
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEY, null)
)
assertEquals(
updatedKeyId,
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEY_ID, null)
val updatedStoredKeysJson =
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEYS, "{}") ?: "{}"
val updatedStoredKeys: Map<String, PushKey> = gson.fromJson(
updatedStoredKeysJson,
object : TypeToken<Map<String, PushKey>>() {}.type
)
assertTrue(updatedStoredKeys.containsKey(updatedKeyId))
assertEquals(updatedKeyId, updatedStoredKeys[updatedKeyId]?.id)
assertEquals(updatedKey, updatedStoredKeys[updatedKeyId]?.value)
}
@Test
fun `emit key1 to value1 and then update key1 to value2`() = runTest(dispatcher) {
val key1 = "key1"
val value1 = "value11"
val value2 = "value22"
// Simulate the event emission from the channel
val channelFlow = MutableSharedFlow<PushKeyUpdate>(replay = 0)
mockChannel.stub {
on { observe() } doReturn channelFlow
}
// Start observing the channel (create provider)
createPushKeyProvider() // This will start observing the channel
dispatcher.scheduler.advanceUntilIdle() // Allow the observation coroutine to run
// Emit first event: (key1 to value1)
channelFlow.emit(PushKeyUpdate(encryptionKey = value1, encryptionKeyId = key1))
dispatcher.scheduler.advanceUntilIdle() // Allow the processing coroutine to run
// Verify first event: (key1 to value1) has been saved in SharedPreferences
val storedKeysJson1 =
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEYS, "{}") ?: "{}"
val storedKeys1: Map<String, PushKey> = Gson().fromJson(
storedKeysJson1,
object : TypeToken<Map<String, PushKey>>() {}.type
)
assertTrue(storedKeys1.containsKey(key1))
assertEquals(key1, storedKeys1[key1]?.id)
assertEquals(value1, storedKeys1[key1]?.value)
assertEquals(1, storedKeys1.size) // Only one key should be present
// Emit second event: (key1 to value2)
channelFlow.emit(PushKeyUpdate(encryptionKey = value2, encryptionKeyId = key1))
dispatcher.scheduler.advanceUntilIdle() // Allow the processing coroutine to run
// Verify second event: (key1 to value2) has updated the value for the same key in SharedPreferences
val storedKeysJson2 =
sharedPreferences.getString(PushKeyProviderImpl.PREF_PUSH_KEYS, "{}") ?: "{}"
val storedKeys2: Map<String, PushKey> = Gson().fromJson(
storedKeysJson2,
object : TypeToken<Map<String, PushKey>>() {}.type
)
// Ensure the value for key1 has been updated to value2
assertTrue(storedKeys2.containsKey(key1))
assertEquals(key1, storedKeys2[key1]?.id) // Verify the updated value
assertEquals(value2, storedKeys2[key1]?.value)
// Ensure no other keys were affected
assertEquals(1, storedKeys2.size) // Still only one key should be present
}
private fun createPushKeyProvider(): PushKeyProviderImpl {
return PushKeyProviderImpl(
sharedPreferences = sharedPreferences,
@ -174,7 +221,8 @@ class PushKeyProviderImplTest {
main = dispatcher,
computation = dispatcher
),
scope = testScope
scope = testScope,
gson = gson
)
}