Fixing wrong database entries & making the equivalent value currency dynamic

- Purging invalid entries of the 'equivalent_values' table
- Displaying whathever currency the user happen to have selected *at the moment*. The previous implementation had a flaw that would have made the currency that was used to calculate the equivalent values in first stick, even if the user changed to another locale settings later.
master
Nelson R. Perez 2019-11-06 16:53:39 -05:00
parent 1422ed699e
commit 8f70b3965d
10 changed files with 119 additions and 27 deletions

View File

@ -4,6 +4,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
@ -12,6 +13,7 @@ import android.preference.PreferenceManager
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.crashlytics.android.Crashlytics
@ -47,6 +49,7 @@ import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.concurrent.thread
/**
* The app uses the single Activity methodology, but this activity was created so that MainActivity can extend from it.
@ -175,6 +178,20 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
this.handleError(it)
})
mCompositeDisposable.add(disposable)
thread {
val info = this.packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
val versionCode = PackageInfoCompat.getLongVersionCode(info)
val hasPurgedEquivalentValues = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(Constants.KEY_HAS_PURGED_EQUIVALENT_VALUES, false)
if(versionCode > 11 && !hasPurgedEquivalentValues) {
mConnectedActivityViewModel.purgeEquivalentValues()
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putBoolean(Constants.KEY_HAS_PURGED_EQUIVALENT_VALUES, true)
.apply()
}
}
}
/**

View File

@ -13,4 +13,10 @@ interface EquivalentValueDao {
@Query("SELECT * FROM equivalent_values")
fun getAllEquivalentValues(): LiveData<List<EquivalentValue>>
@Query("SELECT COUNT(*) FROM equivalent_values WHERE symbol=:currency")
fun getEquivalentValuesCountForCurrency(currency: String): Long
@Query("DELETE FROM equivalent_values WHERE value < 0")
fun purge(): Int
}

View File

@ -42,8 +42,8 @@ interface TransferDetailDao {
assets.precision AS `assetPrecision`,
assets.symbol AS `assetSymbol`,
assets.issuer as `assetIssuer`,
(SELECT value FROM equivalent_values WHERE equivalent_values.transfer_id=transfers.id) AS `fiatAmount`,
(SELECT symbol FROM equivalent_values WHERE equivalent_values.transfer_id=transfers.id) AS `fiatSymbol`
(SELECT value FROM equivalent_values WHERE equivalent_values.transfer_id=transfers.id AND symbol=:currency) AS `fiatAmount`,
(SELECT symbol FROM equivalent_values WHERE equivalent_values.transfer_id=transfers.id AND symbol=:currency) AS `fiatSymbol`
FROM
transfers
INNER JOIN
@ -51,5 +51,5 @@ interface TransferDetailDao {
WHERE
transfers.transfer_asset_id = assets.id
""")
fun getAll(userId: String): LiveData<List<TransferDetail>>
fun getAll(userId: String, currency: String): LiveData<List<TransferDetail>>
}

View File

@ -1,7 +1,7 @@
package cy.agorise.bitsybitshareswallet.network
import retrofit2.Call
import cy.agorise.bitsybitshareswallet.models.coingecko.HistoricalPrice
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Query
@ -13,4 +13,8 @@ interface CoingeckoService {
fun getHistoricalValueSync(@Query("id") id: String,
@Query("date") date: String,
@Query("localization") localization: Boolean): Call<HistoricalPrice>
@Headers("Content-Type: application/json")
@GET("/api/v3/simple/supported_vs_currencies")
fun getSupportedCurrencies(): Call<Array<String>>
}

View File

@ -15,4 +15,8 @@ class EquivalentValuesRepository(context: Context) {
mEquivalentValuesDao = db?.equivalentValueDao()
mTransfersDao = db?.transferDao()
}
fun purge(): Int? {
return mEquivalentValuesDao?.purge()
}
}

View File

@ -3,24 +3,27 @@ package cy.agorise.bitsybitshareswallet.repositories
import android.content.Context
import androidx.lifecycle.LiveData
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.daos.EquivalentValueDao
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetailDao
class TransferDetailRepository internal constructor(context: Context) {
private val mTransferDetailDao: TransferDetailDao
private val mEquivalentValuesDao: EquivalentValueDao
init {
val db = BitsyDatabase.getDatabase(context)
mTransferDetailDao = db!!.transferDetailDao()
mEquivalentValuesDao = db.equivalentValueDao()
}
fun get(userId: String, transferId: String): LiveData<TransferDetail> {
return mTransferDetailDao.get(userId, transferId)
}
fun getAll(userId: String): LiveData<List<TransferDetail>> {
return mTransferDetailDao.getAll(userId)
fun getAll(userId: String, currency: String): LiveData<List<TransferDetail>> {
return mTransferDetailDao.getAll(userId, currency)
}
}

View File

@ -12,9 +12,9 @@ import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import cy.agorise.bitsybitshareswallet.network.CoingeckoService
import cy.agorise.bitsybitshareswallet.network.ServiceGenerator
import cy.agorise.bitsybitshareswallet.utils.Constants
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import java.text.SimpleDateFormat
import java.util.*
@ -77,18 +77,16 @@ class TransferRepository internal constructor(context: Context) {
*/
fun observeMissingEquivalentValuesIn(symbol: String) {
compositeDisposable.add(mTransferDao.getTransfersWithMissingValueIn(symbol)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.map { transfer -> obtainFiatValue(transfer, symbol) }
.subscribe({
Log.d(TAG,"Got equivalent value: $it")
if(it.value >= 0) mEquivalentValuesDao.insert(it)
},{
Log.e(TAG,"Error while trying to create a new equivalent value. Msg: ${it.message}")
for(element in it.stackTrace){
Log.e(TAG,"${element.className}#${element.methodName}:${element.lineNumber}")
}
}))
})
)
}
/**
@ -110,13 +108,7 @@ class TransferRepository internal constructor(context: Context) {
?.execute()
var equivalentFiatValue = -1L
if(response?.isSuccessful == true){
// We try to use the locale-selected currency first
var selectedCurrency = symbol
// Checking that the provided currency is supported by the Coingecko API
val isCurrencySupported = response?.body()?.market_data?.current_price?.keys?.contains(symbol.toLowerCase())
// In case it is not, we fallback to USD
if(isCurrencySupported == false) selectedCurrency = "usd"
val price: Double = response.body()?.market_data?.current_price?.get(selectedCurrency.toLowerCase()) ?: -1.0
val price: Double = response.body()?.market_data?.current_price?.get(symbol.toLowerCase()) ?: -1.0
if(price > 0){
// The equivalent value is obtained by:
// 1- Dividing the base value by 100000 (BTS native precision)
@ -168,4 +160,27 @@ class TransferRepository internal constructor(context: Context) {
if(!compositeDisposable.isDisposed)
compositeDisposable.clear()
}
/**
* Method used to override a given currency if it turns out not to be supported by the API.
* <p>
* The CoinGecko API supports 20+ fiat currencies. So we can very easily calculate historical
* equivalent values for those currencies. If the currency is not supported though,
* we must fall back to USD.
*
* @param symbol The 3 letters symbol of the currency
*/
fun getSupportedCurrency(symbol: String): Observable<String> {
return Observable.just(symbol)
.map {
val sg = ServiceGenerator(Constants.COINGECKO_URL)
val response = sg.getService(CoingeckoService::class.java)
?.getSupportedCurrencies()
?.execute()
if(response?.body()?.indexOf(symbol.toLowerCase()) == -1)
"usd"
else
it
}
}
}

View File

@ -128,7 +128,10 @@ object Constants {
/** Minimum time period in seconds between BitShares nodes list updates */
const val NODES_UPDATE_PERIOD = (60 * 60).toLong() // 1 hour
/** Because of a bug in pre-version code 11 releases, some entries in the equivalent values
* table were invalid. We'll be performing a purge at version code 12, but we only want to do
* it once. After this, we record this in shared preferences using this key */
const val KEY_HAS_PURGED_EQUIVALENT_VALUES = "key_has_purged_equivalent_values"
/////////////////////// Crashlytics custom keys ///////////////////////
/** Key used to add the last visited fragment name to the Crashlytics report */

View File

@ -1,15 +1,21 @@
package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.repositories.EquivalentValuesRepository
import cy.agorise.bitsybitshareswallet.repositories.NodeRepository
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
import cy.agorise.graphenej.network.FullNode
import io.reactivex.schedulers.Schedulers
class ConnectedActivityViewModel(application: Application) : AndroidViewModel(application) {
companion object {
val TAG = "ConnectedActivityVM"
}
private var mTransfersRepository = TransferRepository(application)
private var mEquivalentValuesRep = EquivalentValuesRepository(application)
private val mNodeRepository: NodeRepository
init {
@ -18,7 +24,17 @@ class ConnectedActivityViewModel(application: Application) : AndroidViewModel(ap
}
fun observeMissingEquivalentValuesIn(symbol: String) {
mTransfersRepository.observeMissingEquivalentValuesIn(symbol)
mTransfersRepository.getSupportedCurrency(symbol)
.observeOn(Schedulers.io())
.subscribeOn(Schedulers.io())
.subscribe({
currency -> mTransfersRepository.observeMissingEquivalentValuesIn(currency)
},{
Log.e(TAG,"Error while trying to subscribe to missing equivalent values observer. Msg: ${it.message}")
for(element in it.stackTrace){
Log.e(TAG,"${element.className}#${element.methodName}:${element.lineNumber}")
}
})
}
fun updateNodeLatencies(nodes: List<FullNode>) {
@ -29,4 +45,8 @@ class ConnectedActivityViewModel(application: Application) : AndroidViewModel(ap
super.onCleared()
mTransfersRepository.onCleared()
}
fun purgeEquivalentValues(): Int? {
return mEquivalentValuesRep.purge()
}
}

View File

@ -1,17 +1,25 @@
package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import cy.agorise.bitsybitshareswallet.models.FilterOptions
import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
class TransactionsViewModel(application: Application) : AndroidViewModel(application) {
companion object {
val TAG = "TransactionsViewModel"
}
private var mRepository = TransferDetailRepository(application)
private var mTransfersRepository = TransferRepository(application)
/**
* [FilterOptions] used to filter the list of [TransferDetail] taken from the database
@ -35,13 +43,25 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
}
internal fun getFilteredTransactions(userId: String): LiveData<List<TransferDetail>> {
transactions = mRepository.getAll(userId)
val currency = Currency.getInstance(Locale.getDefault())
mTransfersRepository.getSupportedCurrency(currency.currencyCode)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Log.d(TAG,"Looking for currency with code: ${it}")
transactions = mRepository.getAll(userId, it)
filteredTransactions.addSource(transactions) { transactions ->
viewModelScope.launch {
filteredTransactions.value = filter(transactions, mFilterOptions)
}
}
filteredTransactions.addSource(transactions) { transactions ->
viewModelScope.launch {
filteredTransactions.value = filter(transactions, mFilterOptions)
}
}
},{
Log.e(TAG,"Error while trying to obtain a filtered list of transactions. Msg: ${it.message}")
for(element in it.stackTrace){
Log.e(ConnectedActivityViewModel.TAG,"${element.className}#${element.methodName}:${element.lineNumber}")
}
})
return filteredTransactions
}