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.
master
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.repositories.AssetRepository
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.Helper
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.ConnectedActivityViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.TransferViewModel
@ -121,7 +122,9 @@ abstract class ConnectedActivity : AppCompatActivity() {
mConnectedActivityViewModel = ViewModelProviders.of(this).get(ConnectedActivityViewModel::class.java)
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
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.models.FilterOptions
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.Helper
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
import cy.agorise.bitsybitshareswallet.views.DatePickerFragment
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 }
cbEquivalentValue.isChecked = mFilterOptions.equivalentValueAll
// TODO obtain user selected currency
val currencySymbol = "usd"
mCurrency = Currency.getInstance(currencySymbol)
val currency = Currency.getInstance(Locale.getDefault())
val currencyCode = Helper.getCoingeckoSupportedCurrency(currency.currencyCode)
mCurrency = Currency.getInstance(currencyCode)
val fromEquivalentValue = mFilterOptions.fromEquivalentValue /
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()
etToEquivalentValue.setText("$toEquivalentValue", TextView.BufferType.EDITABLE)
tvEquivalentValueSymbol.text = currencySymbol.toUpperCase()
tvEquivalentValueSymbol.text = currencyCode.toUpperCase(Locale.getDefault())
// Initialize transaction network fees
switchAgoriseFees.isChecked = mFilterOptions.agoriseFees

View File

@ -13,8 +13,4 @@ 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

@ -14,7 +14,6 @@ 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 java.text.SimpleDateFormat
@ -164,41 +163,4 @@ 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()
// 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.FileOutputStream
import java.io.IOException
import java.util.*
/**
* 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)
/**
* 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
}
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 {
// 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")
// Send intent to share image+text
val imagePath = File(context.cacheDir, "images")
val newFile = File(imagePath, "image.png")
val stream = FileOutputStream(cachePath.toString() + "/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)
}
// Create and return image uri
return FileProvider.getUriForFile(context, "cy.agorise.bitsybitshareswallet.FileProvider", newFile)
}
// Send intent to share image+text
val imagePath = File(context.cacheDir, "images")
val newFile = File(imagePath, "image.png")
/**
* If the given currency code is supported, returns it, else returns the default one.
*/
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 FileProvider.getUriForFile(context, "cy.agorise.bitsybitshareswallet.FileProvider", newFile)
}
return if (currencyCode.toLowerCase(Locale.ROOT) in supportedCurrencies)
currencyCode
else
"usd"
}
}

View File

@ -1,14 +1,12 @@
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 {
@ -24,17 +22,7 @@ class ConnectedActivityViewModel(application: Application) : AndroidViewModel(ap
}
fun observeMissingEquivalentValuesIn(symbol: String) {
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}")
}
})
mTransfersRepository.observeMissingEquivalentValuesIn(symbol)
}
fun updateNodeLatencies(nodes: List<FullNode>) {

View File

@ -1,14 +1,11 @@
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 cy.agorise.bitsybitshareswallet.utils.Helper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -16,10 +13,9 @@ import java.util.*
class TransactionsViewModel(application: Application) : AndroidViewModel(application) {
companion object {
val TAG = "TransactionsViewModel"
const 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
@ -44,24 +40,14 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
internal fun getFilteredTransactions(userId: String): LiveData<List<TransferDetail>> {
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)
val currencyCode = Helper.getCoingeckoSupportedCurrency(currency.currencyCode)
transactions = mRepository.getAll(userId, currencyCode)
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}")
}
})
filteredTransactions.addSource(transactions) { transactions ->
viewModelScope.launch {
filteredTransactions.value = filter(transactions, mFilterOptions)
}
}
return filteredTransactions
}