diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt index ff605b0138..7d45e11efc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt @@ -1,12 +1,20 @@ package com.tangem.datasource.di -import com.tangem.datasource.local.datastore.RuntimeDataStore +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi import com.tangem.datasource.local.quote.DefaultQuotesStore import com.tangem.datasource.local.quote.QuotesStore +import com.tangem.datasource.local.quote.utils.QuotesSerializer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -15,9 +23,17 @@ internal object QuotesStoreModule { @Provides @Singleton - fun provideQuotesStore(): QuotesStore { + fun provideQuotesStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): QuotesStore { return DefaultQuotesStore( - dataStore = RuntimeDataStore(), + dataStore = DataStoreFactory.create( + serializer = QuotesSerializer(moshi), + produceFile = { context.dataStoreFile(fileName = "quotes") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index 962b4afddd..3bad4c835a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -1,42 +1,55 @@ package com.tangem.datasource.local.quote +import androidx.datastore.core.DataStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.datasource.local.datastore.core.StringKeyDataStore -import com.tangem.datasource.local.quote.model.StoredQuote +import com.tangem.datasource.local.quote.model.QuoteDM +import com.tangem.datasource.local.quote.model.QuotesDM import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.flow.* +import com.tangem.domain.tokens.model.Quote +import com.tangem.utils.extensions.orZero +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map internal class DefaultQuotesStore( - private val dataStore: StringKeyDataStore, + private val dataStore: DataStore, ) : QuotesStore { - override fun get(currenciesIds: Set): Flow> { - return channelFlow { - val flows = currenciesIds.map { currencyId -> dataStore.get(currencyId.value) } + override fun get(currenciesIds: Set): Flow> { + return dataStore.data + .map { quotes -> createQuotes(currenciesIds, quotes) } + } - if (dataStore.isEmpty() || flows.isEmpty()) { - send(emptySet()) - } + override suspend fun getSync(currenciesIds: Set): Set { + val quotes = dataStore.data.firstOrNull().orEmpty() - merge(*flows.toTypedArray()) - .scan>(emptySet()) { acc, quote -> - acc.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } + return createQuotes(currenciesIds, quotes) + } + + private fun createQuotes(currenciesIds: Set, quoteEntities: Set): Set = + currenciesIds.mapTo(mutableSetOf()) { id -> + quoteEntities.firstOrNull { it.rawCurrencyId == id } + ?.let { quote -> + Quote.Value( + rawCurrencyId = id, + fiatRate = quote.fiatRate, + priceChange = quote.priceChange, + ) } - .filter(Set::isNotEmpty) - .collect(::send) + ?: Quote.Empty(id) } - } - - override suspend fun getSync(currenciesIds: Set): Set { - return currenciesIds.mapNotNull { currencyId -> dataStore.getSyncOrNull(currencyId.value) }.toSet() - } override suspend fun store(response: QuotesResponse) { - val quotes = response.quotes.mapValues { (id, quote) -> - StoredQuote(id, quote) + val newQuotes = response.quotes.mapTo(mutableSetOf()) { (currencyId, quote) -> + QuoteDM( + rawCurrencyId = CryptoCurrency.RawID(currencyId), + fiatRate = quote.price.orZero(), + priceChange = quote.priceChange24h.orZero().movePointLeft(2), + ) } - dataStore.store(quotes) + dataStore.updateData { storedQuotes -> + (newQuotes + storedQuotes).distinctBy { it.rawCurrencyId }.toSet() + } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt index 38f90100e4..2d2fada694 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt @@ -1,15 +1,15 @@ package com.tangem.datasource.local.quote import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.datasource.local.quote.model.StoredQuote import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote import kotlinx.coroutines.flow.Flow interface QuotesStore { - fun get(currenciesIds: Set): Flow> + fun get(currenciesIds: Set): Flow> - suspend fun getSync(currenciesIds: Set): Set + suspend fun getSync(currenciesIds: Set): Set suspend fun store(response: QuotesResponse) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/QuoteDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/QuoteDM.kt new file mode 100644 index 0000000000..1465465631 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/QuoteDM.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.local.quote.model + +import com.squareup.moshi.JsonClass +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +internal typealias QuotesDM = Set + +@JsonClass(generateAdapter = true) +internal data class QuoteDM( + val rawCurrencyId: CryptoCurrency.RawID, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/StoredQuote.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/StoredQuote.kt deleted file mode 100644 index 92397d1a81..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/StoredQuote.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.datasource.local.quote.model - -import com.squareup.moshi.Json -import com.tangem.datasource.api.tangemTech.models.QuotesResponse - -data class StoredQuote( - @Json(name = "rawCurrencyId") val rawCurrencyId: String, - @Json(name = "quote") val quote: QuotesResponse.Quote, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/utils/QuotesSerializer.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/utils/QuotesSerializer.kt new file mode 100644 index 0000000000..524683ce7d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/utils/QuotesSerializer.kt @@ -0,0 +1,32 @@ +package com.tangem.datasource.local.quote.utils + +import androidx.datastore.core.Serializer +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.datasource.local.quote.model.QuoteDM +import com.tangem.datasource.local.quote.model.QuotesDM +import java.io.InputStream +import java.io.OutputStream + +internal class QuotesSerializer(moshi: Moshi) : Serializer { + + private val adapter by lazy { + val types = Types.newParameterizedType(Set::class.java, QuoteDM::class.java) + + moshi.adapter(types) + } + + override val defaultValue: QuotesDM = emptySet() + + override suspend fun readFrom(input: InputStream): QuotesDM { + return input.bufferedReader().use { reader -> + adapter.fromJson(reader.readText()) ?: defaultValue + } + } + + override suspend fun writeTo(t: QuotesDM, output: OutputStream) { + output.bufferedWriter().use { write -> + write.write(adapter.toJson(t)) + } + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 88f9109200..c9b3f2ce64 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -2,7 +2,6 @@ package com.tangem.data.tokens.repository import com.tangem.data.common.api.safeApiCallWithTimeout import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.utils.QuotesConverter import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse @@ -29,7 +28,6 @@ internal class DefaultQuotesRepository( private val dispatchers: CoroutineDispatcherProvider, ) : QuotesRepository { - private val quotesConverter = QuotesConverter() private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter() @Volatile @@ -37,7 +35,8 @@ internal class DefaultQuotesRepository( private val mutex = Mutex() override fun getQuotesUpdates(currenciesIds: Set): Flow> { - TODO("Will be implemented in [REDACTED_TASK_KEY]") + return quotesStore.get(currenciesIds) + .flowOn(dispatchers.io) } override suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { @@ -62,7 +61,7 @@ internal class DefaultQuotesRepository( .filterNotNull() .flatMapLatest { appCurrency -> fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh) - quotesStore.get(currenciesIds).map { quotesConverter.convert(currenciesIds to it) } + quotesStore.get(currenciesIds) } .cancellable() .flowOn(dispatchers.io) @@ -79,17 +78,15 @@ internal class DefaultQuotesRepository( fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh) - val quotes = quotesStore.getSync(currenciesIds) - - quotesConverter.convert(currenciesIds to quotes) + quotesStore.getSync(currenciesIds) } } override suspend fun getQuoteSync(currencyId: CryptoCurrency.RawID): Quote? { return withContext(dispatchers.io) { val setOfCurrencyId = setOf(currencyId) - val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull() - quote?.let { quotesConverter.convert(setOfCurrencyId to setOf(it)).firstOrNull() } + + quotesStore.getSync(setOfCurrencyId).firstOrNull() } } @@ -134,6 +131,7 @@ internal class DefaultQuotesRepository( response, replacementIdsResult.idsFiltered, ) + quotesStore.store(updatedResponse) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt deleted file mode 100644 index ec86873987..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.datasource.local.quote.model.StoredQuote -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Quote -import com.tangem.utils.converter.Converter -import java.math.BigDecimal - -typealias QuotesConverterValue = Pair, Set> - -internal class QuotesConverter : Converter> { - - override fun convert(value: QuotesConverterValue): Set { - val (setOfCurrencyId, setOfStoredQuote) = value - return setOfCurrencyId.mapTo(hashSetOf()) { id -> - setOfStoredQuote.find { id.value == it.rawCurrencyId } - ?.let(::convertExistStoredQuote) - ?: Quote.Empty(id) - } - } - - private fun convertExistStoredQuote(storedQuote: StoredQuote): Quote { - val (rawCurrencyId, responseQuote) = storedQuote - - return Quote.Value( - rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId), - fiatRate = responseQuote.price ?: BigDecimal.ZERO, - priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), - ) - } -} \ No newline at end of file