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:
parent
1ef93d72db
commit
11fd1abeb2
6 changed files with 128 additions and 59 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -50,6 +50,8 @@ dependencies {
|
|||
|
||||
compileOnly libs.javaxInject
|
||||
|
||||
implementation libs.gson
|
||||
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.kotlinTest
|
||||
testImplementation libs.mockitoKotlin
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue