Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-15 14:08:30 +03:00
parent 2a0e8f78a6
commit 7aa44c4da7
17 changed files with 364 additions and 61 deletions

View file

@ -1,10 +1,18 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.appcurrency.MockSelectedAppCurrencyStore
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.implementation.DefaultSelectedAppCurrencyStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@ -14,7 +22,24 @@ internal object AppCurrencyDataModule {
@Provides
@Singleton
fun provideSelectedAppCurrencyStore(): SelectedAppCurrencyStore {
return MockSelectedAppCurrencyStore()
fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore {
return DefaultAvailableAppCurrenciesStore(
dataStore = RuntimeDataStore(),
)
}
@Provides
@Singleton
fun provideSelectedAppCurrencyStore(
@ApplicationContext context: Context,
@NetworkMoshi moshi: Moshi,
): SelectedAppCurrencyStore {
return DefaultSelectedAppCurrencyStore(
dataStore = SharedPreferencesDataStore(
preferencesName = "selected_app_currency",
context = context,
adapter = moshi.adapter(CurrenciesResponse.Currency::class.java),
),
)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.appcurrency
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
interface AvailableAppCurrenciesStore {
suspend fun getAllSyncOrNull(): List<CurrenciesResponse.Currency>?
suspend fun getSyncOrNull(key: String): CurrenciesResponse.Currency?
suspend fun store(response: CurrenciesResponse)
}

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.local.appcurrency.implementation
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultAvailableAppCurrenciesStore(
private val dataStore: StringKeyDataStore<CurrenciesResponse.Currency>,
) : AvailableAppCurrenciesStore,
StringKeyDataStoreDecorator<String, CurrenciesResponse.Currency>(dataStore) {
override fun provideStringKey(key: String): String {
return key
}
override suspend fun store(response: CurrenciesResponse) {
val currencies = response.currencies.associateBy(CurrenciesResponse.Currency::code)
dataStore.store(currencies)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.appcurrency.implementation
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
internal class DefaultSelectedAppCurrencyStore(
dataStore: StringKeyDataStore<CurrenciesResponse.Currency>,
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore)

View file

@ -3,38 +3,49 @@ package com.tangem.datasource.local.datastore
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.model.WriteTrigger
import kotlinx.coroutines.channels.BufferOverflow
import com.tangem.datasource.local.datastore.utils.Trigger
import kotlinx.coroutines.flow.*
import timber.log.Timber
@Deprecated("Use shared preferences data store instead")
internal class FileDataStore<Value : Any>(
private val fileReader: FileReader,
private val adapter: JsonAdapter<Value>,
) : StringKeyDataStore<Value> {
private val writeTrigger = MutableSharedFlow<WriteTrigger>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val writeTrigger = Trigger()
override fun get(key: String): Flow<Value> {
return writeTrigger
.onEmpty { emit(WriteTrigger) }
.map { getInternal(key) }
.filterNotNull()
.distinctUntilChanged()
}
override fun getAll(): Flow<List<Value>> {
val e = NotImplementedError("`getAll()` function not implemented for `FileDataStore`")
Timber.e(e)
throw e
}
override suspend fun getSyncOrNull(key: String): Value? {
return getInternal(key)
}
override suspend fun getAllSyncOrNull(): List<Value> {
val e = NotImplementedError("`getAllSyncOrNull()` function not implemented for `FileDataStore`")
Timber.e(e)
throw e
}
override suspend fun store(key: String, item: Value) {
try {
val json = adapter.toJson(item)
fileReader.rewriteFile(json, key)
writeTrigger.tryEmit(WriteTrigger)
writeTrigger.trigger()
} catch (e: Throwable) {
Timber.e(e, "Unable to write file: $key")
}
@ -48,10 +59,14 @@ internal class FileDataStore<Value : Any>(
override suspend fun remove(key: String) {
fileReader.removeFile(key)
writeTrigger.trigger()
}
override suspend fun clear() {
// TODO: Implement if needed
val e = NotImplementedError("`clear()` function not implemented for `FileDataStore`")
Timber.e(e)
throw e
}
private fun getInternal(fileName: String): Value? {

View file

@ -13,10 +13,18 @@ internal class RuntimeDataStore<Data : Any> : StringKeyDataStore<Data> {
.filterNotNull()
}
override fun getAll(): Flow<List<Data>> {
return store.map { value -> value.values.toList() }
}
override suspend fun getSyncOrNull(key: String): Data? {
return store.value[key]
}
override suspend fun getAllSyncOrNull(): List<Data> {
return store.value.values.toList()
}
override suspend fun store(key: String, item: Data) {
store.update { value ->
value[key] = item

View file

@ -2,12 +2,15 @@ package com.tangem.datasource.local.datastore
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.model.WriteTrigger
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import com.tangem.datasource.local.datastore.utils.Trigger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import timber.log.Timber
internal class SharedPreferencesDataStore<Value : Any>(
@ -16,32 +19,39 @@ internal class SharedPreferencesDataStore<Value : Any>(
private val adapter: JsonAdapter<Value>,
) : StringKeyDataStore<Value> {
private val sharedPreferences by lazy {
private val sharedPreferences: SharedPreferences by lazy {
context.getSharedPreferences(preferencesName, MODE_PRIVATE)
}
private val writeTrigger = MutableSharedFlow<WriteTrigger>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val writeTrigger = Trigger()
override fun get(key: String): Flow<Value> {
return writeTrigger
.onEmpty { emit(WriteTrigger) }
.map { getInternal(key) }
.filterNotNull()
.distinctUntilChanged()
}
override fun getAll(): Flow<List<Value>> {
return writeTrigger
.map { getAllInternal() }
.distinctUntilChanged()
}
override suspend fun getSyncOrNull(key: String): Value? {
return getInternal(key)
}
override suspend fun getAllSyncOrNull(): List<Value> {
return getAllInternal()
}
override suspend fun store(key: String, item: Value) {
try {
val json = adapter.toJson(item)
sharedPreferences.edit { putString(key, json) }
writeTrigger.emit(WriteTrigger)
writeTrigger.trigger()
} catch (e: Throwable) {
Timber.e(e, "Unable to edit preferences: $key")
}
@ -54,13 +64,13 @@ internal class SharedPreferencesDataStore<Value : Any>(
}
override suspend fun remove(key: String) {
sharedPreferences.edit {
remove(key)
}
sharedPreferences.edit { remove(key) }
writeTrigger.trigger()
}
override suspend fun clear() {
sharedPreferences.edit { clear() }
writeTrigger.trigger()
}
private fun getInternal(key: String): Value? {
@ -73,4 +83,17 @@ internal class SharedPreferencesDataStore<Value : Any>(
null
}
}
private fun getAllInternal(): List<Value> {
return sharedPreferences.all.mapNotNull { (key, value) ->
try {
val json = value as? String ?: return@mapNotNull null
adapter.fromJson(json)
} catch (e: Throwable) {
Timber.e(e, "Unable to convert value from JSON: $key")
null
}
}
}
}

View file

@ -6,8 +6,12 @@ internal interface DataStore<Key : Any, Value : Any> {
fun get(key: Key): Flow<Value>
fun getAll(): Flow<List<Value>>
suspend fun getSyncOrNull(key: Key): Value?
suspend fun getAllSyncOrNull(): List<Value>
suspend fun store(key: Key, item: Value)
suspend fun store(items: Map<Key, Value>)

View file

@ -0,0 +1,28 @@
package com.tangem.datasource.local.datastore.core
import kotlinx.coroutines.flow.Flow
internal abstract class KeylessDataStoreDecorator<Value : Any>(
wrappedDataStore: StringKeyDataStore<Value>,
) : StringKeyDataStoreDecorator<Unit, Value>(wrappedDataStore) {
override fun provideStringKey(key: Unit): String {
return STRING_KEY
}
fun get(): Flow<Value> {
return get(Unit)
}
suspend fun getSyncOrNull(): Value? {
return getSyncOrNull(Unit)
}
suspend fun store(item: Value) {
store(Unit, item)
}
private companion object {
const val STRING_KEY = "key"
}
}

View file

@ -3,34 +3,42 @@ package com.tangem.datasource.local.datastore.core
import kotlinx.coroutines.flow.Flow
internal abstract class StringKeyDataStoreDecorator<Key : Any, Value : Any>(
private val dataStore: StringKeyDataStore<Value>,
private val wrappedDataStore: StringKeyDataStore<Value>,
) : DataStore<Key, Value> {
abstract fun provideStringKey(key: Key): String
override fun get(key: Key): Flow<Value> {
return dataStore.get(provideStringKey(key))
return wrappedDataStore.get(provideStringKey(key))
}
override fun getAll(): Flow<List<Value>> {
return wrappedDataStore.getAll()
}
override suspend fun getAllSyncOrNull(): List<Value> {
return wrappedDataStore.getAllSyncOrNull()
}
override suspend fun getSyncOrNull(key: Key): Value? {
return dataStore.getSyncOrNull(provideStringKey(key))
return wrappedDataStore.getSyncOrNull(provideStringKey(key))
}
override suspend fun store(key: Key, item: Value) {
dataStore.store(provideStringKey(key), item)
wrappedDataStore.store(provideStringKey(key), item)
}
override suspend fun store(items: Map<Key, Value>) {
dataStore.store(
wrappedDataStore.store(
items = items.mapKeys { (key, _) -> provideStringKey(key) },
)
}
override suspend fun remove(key: Key) {
dataStore.remove(provideStringKey(key))
wrappedDataStore.remove(provideStringKey(key))
}
override suspend fun clear() {
dataStore.clear()
wrappedDataStore.clear()
}
}

View file

@ -1,3 +0,0 @@
package com.tangem.datasource.local.datastore.model
internal typealias WriteTrigger = Unit

View file

@ -0,0 +1,35 @@
package com.tangem.datasource.local.datastore.utils
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
/**
* Represents a trigger mechanism to emit values on-demand.
*
* This class provides a mechanism to trigger emissions via the [trigger] method.
*
* @property triggerFlow The internal flow that gets toggled to trigger emissions.
*/
internal class Trigger(
private val triggerFlow: MutableStateFlow<Boolean> = MutableStateFlow(value = false),
) : Flow<Unit> {
/**
* Collects values emitted by this flow.
*
* Overrides the default collection mechanism to emit a [Unit] value whenever [triggerFlow] changes.
*
* @param collector The collector responsible for handling emitted values.
*/
override suspend fun collect(collector: FlowCollector<Unit>): Nothing {
triggerFlow.collect { collector.emit(Unit) }
}
/**
* Triggers an emission.
*/
fun trigger() {
triggerFlow.value = !triggerFlow.value
}
}

View file

@ -30,4 +30,5 @@ dependencies {
/** Other */
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
implementation(deps.jodatime)
}

View file

@ -0,0 +1,105 @@
package com.tangem.data.appcurrency
import com.tangem.data.appcurrency.utils.AppCurrencyConverter
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.withContext
import org.joda.time.Duration
import timber.log.Timber
internal class DefaultAppCurrencyRepository(
private val tangemTechApi: TangemTechApi,
private val availableAppCurrenciesStore: AvailableAppCurrenciesStore,
private val selectedAppCurrencyStore: SelectedAppCurrencyStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : AppCurrencyRepository {
private val appCurrencyConverter = AppCurrencyConverter()
override fun getSelectedAppCurrency(): Flow<AppCurrency> {
return selectedAppCurrencyStore.get()
.onEmpty { fetchDefaultAppCurrency() }
.map(appCurrencyConverter::convert)
.flowOn(dispatchers.io)
}
override suspend fun getAvailableAppCurrencies(): List<AppCurrency> {
return withContext(dispatchers.io) {
fetchAvailableCurrenciesIfExpired()
val currencies = availableAppCurrenciesStore.getAllSyncOrNull()
?.map(appCurrencyConverter::convert)
requireNotNull(currencies) {
"No available currencies stored"
}
}
}
override suspend fun changeAppCurrency(currencyCode: String) {
withContext(dispatchers.io) {
val currency = requireNotNull(availableAppCurrenciesStore.getSyncOrNull(currencyCode)) {
"Unable to find app currency with provided code: $currencyCode"
}
selectedAppCurrencyStore.store(currency)
}
}
private suspend fun fetchDefaultAppCurrency() {
fetchAvailableCurrenciesIfExpired()
changeAppCurrency(DEFAULT_CURRENCY_CODE)
}
private suspend fun fetchAvailableCurrenciesIfExpired() {
cacheRegistry.invokeOnExpire(
key = AVAILABLE_CURRENCIES_CACHE_KEY,
skipCache = false,
expireIn = Duration.standardMinutes(AVAILABLE_CURRENCIES_CACHE_KEY_EXPIRE_MINUTES),
block = { fetchAvailableCurrencies() },
)
}
private suspend fun fetchAvailableCurrencies() {
try {
val response = tangemTechApi.getCurrencyList()
availableAppCurrenciesStore.store(response)
} catch (e: Throwable) {
Timber.e(e, "Unable to fetch available currencies")
availableAppCurrenciesStore.store(getDefaultCurrenciesResponse())
}
}
private fun getDefaultCurrenciesResponse(): CurrenciesResponse = CurrenciesResponse(
currencies = listOf(
CurrenciesResponse.Currency(
id = DEFAULT_CURRENCY_CODE.lowercase(),
code = DEFAULT_CURRENCY_CODE,
name = "US Dollar",
unit = "$",
type = "fiat",
rateBTC = "",
),
),
)
private companion object {
const val AVAILABLE_CURRENCIES_CACHE_KEY = "available_currencies"
const val AVAILABLE_CURRENCIES_CACHE_KEY_EXPIRE_MINUTES = 15L
const val DEFAULT_CURRENCY_CODE = "USD"
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.data.appcurrency
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
internal class MockAppCurrencyRepository : AppCurrencyRepository {
private val mockAppCurrencies = listOf(AppCurrency(code = "USD", name = "US Dollar", symbol = "$"))
override fun getSelectedAppCurrency(): Flow<AppCurrency> {
return flowOf(mockAppCurrencies.first())
}
override suspend fun getAvailableAppCurrencies(): List<AppCurrency> {
return mockAppCurrencies
}
override suspend fun changeAppCurrency(currencyCode: String) {
/* no-op */
}
}

View file

@ -1,7 +1,12 @@
package com.tangem.data.appcurrency.di
import com.tangem.data.appcurrency.MockAppCurrencyRepository
import com.tangem.data.appcurrency.DefaultAppCurrencyRepository
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -14,7 +19,19 @@ internal object AppCurrencyDataModule {
@Provides
@Singleton
fun provideAppCurrencyRepository(): AppCurrencyRepository {
return MockAppCurrencyRepository()
fun provideAppCurrencyRepository(
tangemTechApi: TangemTechApi,
availableAppCurrenciesStore: AvailableAppCurrenciesStore,
selectedAppCurrencyStore: SelectedAppCurrencyStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): AppCurrencyRepository {
return DefaultAppCurrencyRepository(
tangemTechApi,
availableAppCurrenciesStore,
selectedAppCurrencyStore,
cacheRegistry,
dispatchers,
)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.data.appcurrency.utils
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.utils.converter.Converter
internal class AppCurrencyConverter : Converter<CurrenciesResponse.Currency, AppCurrency> {
override fun convert(value: CurrenciesResponse.Currency): AppCurrency {
return AppCurrency(
code = value.code,
name = value.name,
symbol = value.unit,
)
}
}