Avoid crash due to unsupported currency locale.

- Avoided a crash in ConnectedActivity when trying to obtain the Locale's associated currency, when a Locale does not have a currency.
- Standardized the process to obtain the coingecko supported currency, which will first try to use the current locale's currency and fallback to USD in case the first is not supported.
This commit is contained in:
Severiano Jaramillo 2019-12-25 17:15:19 -06:00
parent 766d42386a
commit 9cabc0565a
7 changed files with 66 additions and 117 deletions

View file

@ -19,6 +19,7 @@ import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import cy.agorise.bitsybitshareswallet.processors.TransfersLoader import cy.agorise.bitsybitshareswallet.processors.TransfersLoader
import cy.agorise.bitsybitshareswallet.repositories.AssetRepository import cy.agorise.bitsybitshareswallet.repositories.AssetRepository
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.Helper
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceViewModel import cy.agorise.bitsybitshareswallet.viewmodels.BalanceViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.ConnectedActivityViewModel import cy.agorise.bitsybitshareswallet.viewmodels.ConnectedActivityViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.TransferViewModel import cy.agorise.bitsybitshareswallet.viewmodels.TransferViewModel
@ -121,7 +122,9 @@ abstract class ConnectedActivity : AppCompatActivity() {
mConnectedActivityViewModel = ViewModelProviders.of(this).get(ConnectedActivityViewModel::class.java) mConnectedActivityViewModel = ViewModelProviders.of(this).get(ConnectedActivityViewModel::class.java)
val currency = Currency.getInstance(Locale.getDefault()) val currency = Currency.getInstance(Locale.getDefault())
mConnectedActivityViewModel.observeMissingEquivalentValuesIn(currency.currencyCode) //TODO: Obtain this from shared preferences? val currencyCode = Helper.getCoingeckoSupportedCurrency(currency.currencyCode)
Log.d(TAG, "Using currency: ${currencyCode.toUpperCase(Locale.ROOT)}")
mConnectedActivityViewModel.observeMissingEquivalentValuesIn(currencyCode)
// Configure UserAccountViewModel to obtain the missing account ids // Configure UserAccountViewModel to obtain the missing account ids
mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java) mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java)

View file

@ -18,6 +18,7 @@ import cy.agorise.bitsybitshareswallet.adapters.BalancesDetailsAdapter
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
import cy.agorise.bitsybitshareswallet.models.FilterOptions import cy.agorise.bitsybitshareswallet.models.FilterOptions
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.Helper
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
import cy.agorise.bitsybitshareswallet.views.DatePickerFragment import cy.agorise.bitsybitshareswallet.views.DatePickerFragment
import kotlinx.android.synthetic.main.dialog_filter_options.* import kotlinx.android.synthetic.main.dialog_filter_options.*
@ -158,9 +159,9 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
llEquivalentValue.visibility = if(isChecked) View.GONE else View.VISIBLE } llEquivalentValue.visibility = if(isChecked) View.GONE else View.VISIBLE }
cbEquivalentValue.isChecked = mFilterOptions.equivalentValueAll cbEquivalentValue.isChecked = mFilterOptions.equivalentValueAll
// TODO obtain user selected currency val currency = Currency.getInstance(Locale.getDefault())
val currencySymbol = "usd" val currencyCode = Helper.getCoingeckoSupportedCurrency(currency.currencyCode)
mCurrency = Currency.getInstance(currencySymbol) mCurrency = Currency.getInstance(currencyCode)
val fromEquivalentValue = mFilterOptions.fromEquivalentValue / val fromEquivalentValue = mFilterOptions.fromEquivalentValue /
Math.pow(10.0, mCurrency.defaultFractionDigits.toDouble()).toLong() Math.pow(10.0, mCurrency.defaultFractionDigits.toDouble()).toLong()
@ -170,7 +171,7 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
Math.pow(10.0, mCurrency.defaultFractionDigits.toDouble()).toLong() Math.pow(10.0, mCurrency.defaultFractionDigits.toDouble()).toLong()
etToEquivalentValue.setText("$toEquivalentValue", TextView.BufferType.EDITABLE) etToEquivalentValue.setText("$toEquivalentValue", TextView.BufferType.EDITABLE)
tvEquivalentValueSymbol.text = currencySymbol.toUpperCase() tvEquivalentValueSymbol.text = currencyCode.toUpperCase(Locale.getDefault())
// Initialize transaction network fees // Initialize transaction network fees
switchAgoriseFees.isChecked = mFilterOptions.agoriseFees switchAgoriseFees.isChecked = mFilterOptions.agoriseFees

View file

@ -13,8 +13,4 @@ interface CoingeckoService {
fun getHistoricalValueSync(@Query("id") id: String, fun getHistoricalValueSync(@Query("id") id: String,
@Query("date") date: String, @Query("date") date: String,
@Query("localization") localization: Boolean): Call<HistoricalPrice> @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

@ -14,7 +14,6 @@ import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import cy.agorise.bitsybitshareswallet.network.CoingeckoService import cy.agorise.bitsybitshareswallet.network.CoingeckoService
import cy.agorise.bitsybitshareswallet.network.ServiceGenerator import cy.agorise.bitsybitshareswallet.network.ServiceGenerator
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -164,41 +163,4 @@ class TransferRepository internal constructor(context: Context) {
if(!compositeDisposable.isDisposed) if(!compositeDisposable.isDisposed)
compositeDisposable.clear() 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()
// Updating the supported currencies cache
mPreferences.edit()
.putStringSet(Constants.KEY_COINGECKO_CURRENCIES_CACHE, response?.body()?.toMutableSet() ?: setOf())
.apply()
if(response?.body()?.indexOf(symbol.toLowerCase()) == -1)
"usd"
else
it
}
.onErrorReturn {
// Error caused potentially by the lack of connectivity. If this happens we just
// retrieve the value from the cache
val currencies = mPreferences.getStringSet(Constants.KEY_COINGECKO_CURRENCIES_CACHE, setOf())
var selectedCurrency = "usd"
if(currencies.contains(symbol)) {
selectedCurrency = symbol
}
selectedCurrency
}
}
} }

View file

@ -10,51 +10,64 @@ import androidx.core.content.FileProvider
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.*
/** /**
* Contains methods that are helpful in different parts of the app * Contains methods that are helpful in different parts of the app
*/ */
class Helper { object Helper {
private const val TAG = "Helper"
companion object { /**
private val TAG = "Helper" * Creates and returns a Bitmap from the contents of a View, does not matter
* if it is a simple view or a ViewGroup like a ConstraintLayout or a LinearLayout.
*
* @param view The view that is gonna be pictured.
* @return The generated image from the given view.
*/
fun loadBitmapFromView(view: View): Bitmap {
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)
/** return bitmap
* Creates and returns a Bitmap from the contents of a View, does not matter }
* if it is a simple view or a ViewGroup like a ConstraintLayout or a LinearLayout.
*
* @param view The view that is gonna be pictured.
* @return The generated image from the given view.
*/
fun loadBitmapFromView(view: View): Bitmap {
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap fun saveTemporalBitmap(context: Context, bitmap: Bitmap): Uri {
// save bitmap to cache directory
try {
val cachePath = File(context.cacheDir, "images")
if (!cachePath.mkdirs())
// don't forget to make the directory
Log.d(TAG, "shareBitmapImage creating cache images folder")
val stream = FileOutputStream("$cachePath/image.png") // overwrites this image every time
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
stream.close()
} catch (e: IOException) {
Log.d(TAG, "shareBitmapImage error: " + e.message)
} }
fun saveTemporalBitmap(context: Context, bitmap: Bitmap): Uri { // Send intent to share image+text
// save bitmap to cache directory val imagePath = File(context.cacheDir, "images")
try { val newFile = File(imagePath, "image.png")
val cachePath = File(context.cacheDir, "images")
if (!cachePath.mkdirs())
// don't forget to make the directory
Log.d(TAG, "shareBitmapImage creating cache images folder")
val stream = FileOutputStream(cachePath.toString() + "/image.png") // overwrites this image every time // Create and return image uri
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) return FileProvider.getUriForFile(context, "cy.agorise.bitsybitshareswallet.FileProvider", newFile)
stream.close() }
} catch (e: IOException) {
Log.d(TAG, "shareBitmapImage error: " + e.message)
}
// Send intent to share image+text /**
val imagePath = File(context.cacheDir, "images") * If the given currency code is supported, returns it, else returns the default one.
val newFile = File(imagePath, "image.png") */
fun getCoingeckoSupportedCurrency(currencyCode: String): String {
val supportedCurrencies = setOf("usd", "aed", "ars", "aud", "bdt", "bhd", "bmd", "brl", "cad",
"chf", "clp", "cny", "czk", "dkk", "eur", "gbp", "hkd", "huf", "idr", "ils", "inr", "jpy",
"krw", "kwd", "lkr", "mmk", "mxn", "myr", "nok", "nzd", "php", "pkr", "pln", "rub", "sar",
"sek", "sgd", "thb", "try", "twd", "uah", "vef", "vnd", "zar", "xdr", "xag", "xau")
// Create and return image uri return if (currencyCode.toLowerCase(Locale.ROOT) in supportedCurrencies)
return FileProvider.getUriForFile(context, "cy.agorise.bitsybitshareswallet.FileProvider", newFile) currencyCode
} else
"usd"
} }
} }

View file

@ -1,14 +1,12 @@
package cy.agorise.bitsybitshareswallet.viewmodels package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.repositories.EquivalentValuesRepository import cy.agorise.bitsybitshareswallet.repositories.EquivalentValuesRepository
import cy.agorise.bitsybitshareswallet.repositories.NodeRepository import cy.agorise.bitsybitshareswallet.repositories.NodeRepository
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
import cy.agorise.graphenej.network.FullNode import cy.agorise.graphenej.network.FullNode
import io.reactivex.schedulers.Schedulers
class ConnectedActivityViewModel(application: Application) : AndroidViewModel(application) { class ConnectedActivityViewModel(application: Application) : AndroidViewModel(application) {
companion object { companion object {
@ -24,17 +22,7 @@ class ConnectedActivityViewModel(application: Application) : AndroidViewModel(ap
} }
fun observeMissingEquivalentValuesIn(symbol: String) { fun observeMissingEquivalentValuesIn(symbol: String) {
mTransfersRepository.getSupportedCurrency(symbol) mTransfersRepository.observeMissingEquivalentValuesIn(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>) { fun updateNodeLatencies(nodes: List<FullNode>) {

View file

@ -1,14 +1,11 @@
package cy.agorise.bitsybitshareswallet.viewmodels package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application import android.app.Application
import android.util.Log
import androidx.lifecycle.* import androidx.lifecycle.*
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import cy.agorise.bitsybitshareswallet.models.FilterOptions import cy.agorise.bitsybitshareswallet.models.FilterOptions
import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository import cy.agorise.bitsybitshareswallet.utils.Helper
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -16,10 +13,9 @@ import java.util.*
class TransactionsViewModel(application: Application) : AndroidViewModel(application) { class TransactionsViewModel(application: Application) : AndroidViewModel(application) {
companion object { companion object {
val TAG = "TransactionsViewModel" const val TAG = "TransactionsViewModel"
} }
private var mRepository = TransferDetailRepository(application) private var mRepository = TransferDetailRepository(application)
private var mTransfersRepository = TransferRepository(application)
/** /**
* [FilterOptions] used to filter the list of [TransferDetail] taken from the database * [FilterOptions] used to filter the list of [TransferDetail] taken from the database
@ -44,24 +40,14 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
internal fun getFilteredTransactions(userId: String): LiveData<List<TransferDetail>> { internal fun getFilteredTransactions(userId: String): LiveData<List<TransferDetail>> {
val currency = Currency.getInstance(Locale.getDefault()) val currency = Currency.getInstance(Locale.getDefault())
mTransfersRepository.getSupportedCurrency(currency.currencyCode) val currencyCode = Helper.getCoingeckoSupportedCurrency(currency.currencyCode)
.subscribeOn(Schedulers.io()) transactions = mRepository.getAll(userId, currencyCode)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Log.d(TAG,"Looking for currency with code: ${it}")
transactions = mRepository.getAll(userId, it)
filteredTransactions.addSource(transactions) { transactions -> filteredTransactions.addSource(transactions) { transactions ->
viewModelScope.launch { viewModelScope.launch {
filteredTransactions.value = filter(transactions, mFilterOptions) 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 return filteredTransactions
} }