feat: migrate FiatRateCache to DataStore
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package com.bitcointxoko.gudariwallet.data
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.io.IOException
|
||||
|
||||
private const val TAG = "FiatDataStore"
|
||||
private const val DATASTORE_NAME = "fiat_prefs"
|
||||
|
||||
// Top-level extension — one instance per process
|
||||
private val Context.fiatDataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = DATASTORE_NAME
|
||||
)
|
||||
|
||||
class FiatDataStore(context: Context) {
|
||||
|
||||
private val dataStore = context.applicationContext.fiatDataStore
|
||||
private val gson = Gson()
|
||||
|
||||
// ── Key factories ─────────────────────────────────────────────────────────
|
||||
|
||||
private fun rateKey(currency: String) = floatPreferencesKey("fiat_rate_$currency")
|
||||
private fun rateFetchedAtKey(currency: String) = longPreferencesKey("fiat_rate_fetched_at_$currency")
|
||||
private val currenciesKey = stringPreferencesKey("fiat_currencies_json")
|
||||
|
||||
// ── Rate ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the last persisted rate for [currency], or null if none exists.
|
||||
* Used as a cold-start fallback while a fresh network fetch is in flight.
|
||||
*/
|
||||
suspend fun getPersistedRate(currency: String): Double? {
|
||||
val prefs = dataStore.data
|
||||
.catch { e ->
|
||||
if (e is IOException) {
|
||||
Log.w(TAG, "FIAT [RATE STALE ] read error — ${e.message}")
|
||||
emit(emptyPreferences())
|
||||
} else throw e
|
||||
}
|
||||
.first()
|
||||
val value = prefs[rateKey(currency)]?.toDouble()
|
||||
if (value != null && value > 0.0) {
|
||||
Log.d(TAG, "FIAT [RATE STALE ] $currency — using persisted rate $value")
|
||||
}
|
||||
return value?.takeIf { it > 0.0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a freshly fetched [rate] for [currency].
|
||||
* Always called alongside the in-memory update in [FiatRateCache.putRate].
|
||||
*/
|
||||
suspend fun persistRate(currency: String, rate: Double) {
|
||||
runCatching {
|
||||
dataStore.edit { prefs ->
|
||||
prefs[rateKey(currency)] = rate.toFloat()
|
||||
prefs[rateFetchedAtKey(currency)] = System.currentTimeMillis()
|
||||
}
|
||||
}.onFailure { e ->
|
||||
Log.w(TAG, "FIAT [RATE FRESH ] persist failed for $currency — ${e.message}")
|
||||
}
|
||||
Log.d(TAG, "FIAT [RATE FRESH ] $currency = $rate — persisted to DataStore")
|
||||
}
|
||||
|
||||
// ── Currency list ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the persisted currency list, or null if none has been stored yet.
|
||||
*/
|
||||
suspend fun getPersistedCurrencies(): List<String>? {
|
||||
val prefs = dataStore.data
|
||||
.catch { e ->
|
||||
if (e is IOException) {
|
||||
Log.w(TAG, "FIAT [CURRENCIES] read error — ${e.message}")
|
||||
emit(emptyPreferences())
|
||||
} else throw e
|
||||
}
|
||||
.first()
|
||||
val json = prefs[currenciesKey] ?: return null
|
||||
return runCatching {
|
||||
val type = object : TypeToken<List<String>>() {}.type
|
||||
val list: List<String> = gson.fromJson(json, type)
|
||||
Log.d(TAG, "FIAT [CURRENCIES] DataStore hit (${list.size} currencies)")
|
||||
list
|
||||
}.getOrElse { e ->
|
||||
Log.w(TAG, "FIAT [CURRENCIES] parse failed — ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists [currencies] to DataStore.
|
||||
*/
|
||||
suspend fun persistCurrencies(currencies: List<String>) {
|
||||
if (currencies.isEmpty()) return
|
||||
runCatching {
|
||||
dataStore.edit { prefs ->
|
||||
prefs[currenciesKey] = gson.toJson(currencies)
|
||||
}
|
||||
}.onFailure { e ->
|
||||
Log.w(TAG, "FIAT [CURRENCIES] persist failed — ${e.message}")
|
||||
}
|
||||
Log.d(TAG, "FIAT [CURRENCIES] persisted ${currencies.size} currencies to DataStore")
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
package com.bitcointxoko.gudariwallet.data
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
private const val TAG = "FiatRateCache"
|
||||
|
||||
/**
|
||||
* Two-tier (in-memory + SharedPreferences) cache for fiat exchange rates
|
||||
* Two-tier (in-memory + DataStore) cache for fiat exchange rates
|
||||
* and the supported-currency list.
|
||||
*
|
||||
* Receives the already-open [prefs] instance from [BalancePrefsStore] so that
|
||||
* all wallet prefs stay in a single file.
|
||||
* Disk persistence is delegated to [FiatDataStore]; this class owns
|
||||
* only the in-memory (L1) layer and the cache-coordination logic.
|
||||
*/
|
||||
class FiatRateCache(store: BalancePrefsStore) {
|
||||
private val prefs = store.prefs
|
||||
class FiatRateCache(private val fiatDataStore: FiatDataStore) {
|
||||
|
||||
// ── Rate ─────────────────────────────────────────────────────────────────
|
||||
// ── Rate ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private var cachedRate : Double? = null
|
||||
private var rateLastFetchedAt : Long = 0L
|
||||
@@ -26,9 +21,10 @@ class FiatRateCache(store: BalancePrefsStore) {
|
||||
|
||||
/**
|
||||
* Returns a fresh in-memory rate for [currency] if it is younger than
|
||||
* [ttlMs], then falls back to the last persisted float, then returns null.
|
||||
* [ttlMs], then falls back to the last persisted value via DataStore,
|
||||
* then returns null.
|
||||
*/
|
||||
fun getRate(currency: String, ttlMs: Long): Double? {
|
||||
suspend fun getRate(currency: String, ttlMs: Long): Double? {
|
||||
val now = System.currentTimeMillis()
|
||||
val age = now - rateLastFetchedAt
|
||||
|
||||
@@ -37,9 +33,9 @@ class FiatRateCache(store: BalancePrefsStore) {
|
||||
return cachedRate
|
||||
}
|
||||
|
||||
// Cold-start fallback: last persisted rate
|
||||
val persisted = prefs.getFloat("fiat_rate_$currency", 0f).toDouble()
|
||||
if (persisted > 0.0) {
|
||||
// Cold-start fallback: last persisted rate from DataStore
|
||||
val persisted = fiatDataStore.getPersistedRate(currency)
|
||||
if (persisted != null) {
|
||||
Log.d(TAG, "FIAT [RATE STALE ] $currency — using persisted rate $persisted")
|
||||
return persisted
|
||||
}
|
||||
@@ -50,15 +46,12 @@ class FiatRateCache(store: BalancePrefsStore) {
|
||||
/**
|
||||
* Stores a freshly fetched [rate] for [currency] in memory and on disk.
|
||||
*/
|
||||
fun putRate(currency: String, rate: Double) {
|
||||
suspend fun putRate(currency: String, rate: Double) {
|
||||
val now = System.currentTimeMillis()
|
||||
cachedRate = rate
|
||||
rateLastFetchedAt = now
|
||||
cachedRateCurrency = currency
|
||||
prefs.edit {
|
||||
putFloat("fiat_rate_$currency", rate.toFloat())
|
||||
putLong("fiat_rate_fetched_at_$currency", now)
|
||||
}
|
||||
fiatDataStore.persistRate(currency, rate)
|
||||
Log.d(TAG, "FIAT [RATE FRESH ] 1 $currency = $rate sats — cached + persisted")
|
||||
}
|
||||
|
||||
@@ -72,38 +65,32 @@ class FiatRateCache(store: BalancePrefsStore) {
|
||||
// ── Currency list ─────────────────────────────────────────────────────────
|
||||
|
||||
private var cachedCurrencies: List<String>? = null
|
||||
private val gson = Gson()
|
||||
|
||||
/**
|
||||
* Returns the cached currency list (memory → disk → null).
|
||||
* Returns the cached currency list (memory → DataStore → null).
|
||||
* Callers are responsible for fetching from the network on a null result.
|
||||
*/
|
||||
fun getCurrencies(): List<String>? {
|
||||
suspend fun getCurrencies(): List<String>? {
|
||||
cachedCurrencies?.let {
|
||||
Log.d(TAG, "FIAT [CURRENCIES] in-memory hit (${it.size} currencies)")
|
||||
return it
|
||||
}
|
||||
|
||||
val json = prefs.getString("fiat_currencies_json", null) ?: return null
|
||||
return runCatching {
|
||||
val type = object : TypeToken<List<String>>() {}.type
|
||||
val list: List<String> = gson.fromJson(json, type)
|
||||
Log.d(TAG, "FIAT [CURRENCIES] prefs hit (${list.size} currencies)")
|
||||
list.also { cachedCurrencies = it }
|
||||
}.getOrElse { e ->
|
||||
Log.w(TAG, "FIAT [CURRENCIES] prefs parse failed — ${e.message}")
|
||||
null
|
||||
val persisted = fiatDataStore.getPersistedCurrencies()
|
||||
if (persisted != null) {
|
||||
cachedCurrencies = persisted
|
||||
}
|
||||
return persisted
|
||||
}
|
||||
|
||||
/** Stores [currencies] in memory and on disk. */
|
||||
fun putCurrencies(currencies: List<String>) {
|
||||
suspend fun putCurrencies(currencies: List<String>) {
|
||||
if (currencies.isEmpty()) return
|
||||
cachedCurrencies = currencies
|
||||
prefs.edit { putString("fiat_currencies_json", gson.toJson(currencies)) }
|
||||
fiatDataStore.persistCurrencies(currencies)
|
||||
Log.d(TAG, "FIAT [CURRENCIES] cached ${currencies.size} currencies")
|
||||
}
|
||||
|
||||
val currentRate: Double?
|
||||
get() = cachedRate.takeIf { cachedRateCurrency.isNotBlank() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user