Skip to content

Commit

Permalink
Merge branch 'api-openexchangerates'
Browse files Browse the repository at this point in the history
  • Loading branch information
sal0max committed Feb 8, 2024
2 parents d0530bd + f946556 commit e0207d3
Show file tree
Hide file tree
Showing 23 changed files with 350 additions and 20 deletions.
16 changes: 10 additions & 6 deletions app/src/main/kotlin/de/salomax/currencies/model/ApiProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@ import de.salomax.currencies.model.provider.FerEe
import de.salomax.currencies.model.provider.FrankfurterApp
import de.salomax.currencies.model.provider.InforEuro
import de.salomax.currencies.model.provider.NorgesBank
import de.salomax.currencies.model.provider.OpenExchangerates
import java.time.LocalDate

@JsonClass(generateAdapter = false) // see https://stackoverflow.com/a/64085370/421140
enum class ApiProvider(
val id: Int, // safer ordinal; DON'T CHANGE!
private val implementation: Api,
private val implementation: Api
) {
// EXCHANGERATE_HOST(0, "https://api.exchangerate.host"), // removed, as API was shut down
FRANKFURTER_APP(1, FrankfurterApp()),
FER_EE(2, FerEe()),
INFOR_EURO(3, InforEuro()),
NORGES_BANK(4, NorgesBank()),
BANK_ROSSII(5, BankRossii()),
BANK_OF_CANADA(6, BankOfCanada());
BANK_OF_CANADA(6, BankOfCanada()),
OPEN_EXCHANGERATES(7, OpenExchangerates());

companion object {
fun fromId(value: Int): ApiProvider = entries.firstOrNull { it.id == value }
Expand All @@ -46,16 +48,17 @@ enum class ApiProvider(
fun getHint(context: Context): CharSequence? =
this.implementation.descriptionHint(context)

suspend fun getRates(date: LocalDate?): Result<ExchangeRates, FuelError> =
this.implementation.getRates(date)
suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError> =
this.implementation.getRates(context, date)

suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
endDate: LocalDate
): Result<Timeline, FuelError> {
return this.implementation.getTimeline(base, symbol, startDate, endDate)
return this.implementation.getTimeline(context, base, symbol, startDate, endDate)
}

abstract class Api {
Expand All @@ -66,8 +69,9 @@ enum class ApiProvider(
abstract fun descriptionHint(context: Context): CharSequence?

abstract val baseUrl: String
abstract suspend fun getRates(date: LocalDate?): Result<ExchangeRates, FuelError>
abstract suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError>
abstract suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package de.salomax.currencies.model.adapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import de.salomax.currencies.model.ApiProvider
import de.salomax.currencies.model.Currency
import de.salomax.currencies.model.ExchangeRates
import de.salomax.currencies.model.Rate
import java.io.IOException
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId

@Suppress("unused", "UNUSED_PARAMETER")
internal class OpenExchangeratesRatesAdapter {

@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE", "UNUSED_VALUE")
@Synchronized
@FromJson
@Throws(IOException::class)
fun fromJson(reader: JsonReader): ExchangeRates? {
val rates = mutableListOf<Rate>()
var base: Currency? = null
var date: LocalDate? = null
var errorMessage: String? = null
var errorDescription: String? = null

if (reader.peek() == JsonReader.Token.BEGIN_OBJECT)
reader.beginObject()
else
return null

// parse
while (reader.hasNext()) {
if (reader.peek() == JsonReader.Token.NAME) {
when (reader.nextName()) {
"rates" -> {
reader.beginObject()
// convert
while (reader.hasNext()) {
val name: Currency? = Currency.fromString(reader.nextName())
val value: Float = reader.nextDouble().toFloat()

if (name != null)
rates.add(Rate(name, value))
}
reader.endObject()
}
"timestamp" -> {
date = Instant.ofEpochSecond(reader.nextLong())
.atZone(ZoneId.systemDefault()).toLocalDate()
}
"base" -> {
base = Currency.fromString(reader.nextString())
}
"message" -> {
errorMessage = reader.nextString()
}
"description" -> {
errorDescription = reader.nextString()
}
else -> {
reader.skipValue()
}
}
}
}

reader.endObject()

// also add Faroese króna (same as Danish krone) if it isn't already there - I simply like it!
if (rates.find { it.currency == Currency.FOK } == null)
rates.find { it.currency == Currency.DKK }?.value?.let { dkk ->
rates.add(Rate(Currency.FOK, dkk))
}

return if (rates.isNotEmpty())
ExchangeRates(
success = true,
error = null,
base = base,
date = date,
rates = rates,
provider = ApiProvider.INFOR_EURO
)
// error message
else {
ExchangeRates(
success = false,
error = errorMessage,
base = base,
date = date,
rates = null,
provider = ApiProvider.INFOR_EURO
)
}
}

@Synchronized
@ToJson
@Throws(IOException::class)
fun toJson(writer: JsonWriter, value: ExchangeRates) {
writer.nullValue()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class BankOfCanada: ApiProvider.Api() {

override val baseUrl = "https://www.bankofcanada.ca/valet"

override suspend fun getRates(date: LocalDate?): Result<ExchangeRates, FuelError> {
override suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError> {

// As this API doesn't return results for nonwork days, get the last seven days.
// The latest available values will be used.
Expand Down Expand Up @@ -66,6 +66,7 @@ class BankOfCanada: ApiProvider.Api() {
}

override suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class BankRossii : ApiProvider.Api() {

override val baseUrl = "https://www.cbr.ru/scripts"

override suspend fun getRates(date: LocalDate?): Result<ExchangeRates, FuelError> {
override suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError> {
val dateString =
// latest
if (date == null) ""
Expand All @@ -58,6 +58,7 @@ class BankRossii : ApiProvider.Api() {
}

override suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class FerEe : ApiProvider.Api() {

override val baseUrl = "https://api.fer.ee"

override suspend fun getRates(date: LocalDate?): Result<ExchangeRates, FuelError> {
override suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError> {
// Currency conversions are done relatively to each other - so it basically doesn't matter
// which base is used here. However, Euro is a strong currency, preventing rounding errors.
val base = Currency.EUR
Expand Down Expand Up @@ -66,6 +66,7 @@ class FerEe : ApiProvider.Api() {
}

override suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class FrankfurterApp : ApiProvider.Api() {

override val baseUrl = "https://api.frankfurter.app"

override suspend fun getRates(date: LocalDate?): Result<ExchangeRates, FuelError> {
override suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError> {
// Currency conversions are done relatively to each other - so it basically doesn't matter
// which base is used here. However, Euro is a strong currency, preventing rounding errors.
val base = Currency.EUR
Expand Down Expand Up @@ -66,6 +66,7 @@ class FrankfurterApp : ApiProvider.Api() {
}

override suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class InforEuro : ApiProvider.Api() {

override val baseUrl = "https://ec.europa.eu/budg/inforeuro/api/public"

override suspend fun getRates(date: LocalDate?): Result<ExchangeRates, FuelError> {
override suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError> {
return Fuel.get(
baseUrl +
"/monthly-rates" +
Expand All @@ -60,6 +60,7 @@ class InforEuro : ApiProvider.Api() {
}

override suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class NorgesBank: ApiProvider.Api() {

override val baseUrl = "https://data.norges-bank.no/api"

override suspend fun getRates(date: LocalDate?): Result<ExchangeRates, FuelError> {
override suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError> {
// As this API doesn't return results for nonwork days, get the last seven days.
// The latest available values will be used.
val formattedDateStart = date?.minusDays(7)?.format(DateTimeFormatter.ISO_LOCAL_DATE)
Expand Down Expand Up @@ -63,6 +63,7 @@ class NorgesBank: ApiProvider.Api() {
}

override suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package de.salomax.currencies.model.provider

import android.content.Context
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.fuel.core.awaitResult
import com.github.kittinunf.fuel.moshi.moshiDeserializerOf
import com.github.kittinunf.result.Result
import com.github.kittinunf.result.map
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import de.salomax.currencies.R
import de.salomax.currencies.model.ApiProvider
import de.salomax.currencies.model.Currency
import de.salomax.currencies.model.ExchangeRates
import de.salomax.currencies.model.Timeline
import de.salomax.currencies.model.adapter.OpenExchangeratesRatesAdapter
import de.salomax.currencies.repository.Database
import java.time.LocalDate
import java.time.format.DateTimeFormatter

class OpenExchangerates : ApiProvider.Api() {

override val name = "Open Exchangerates"

override fun descriptionShort(context: Context) =
context.getText(R.string.api_openExchangeRates_descriptionShort)

override fun getDescriptionLong(context: Context) =
context.getText(R.string.api_openExchangeRates_descriptionFull)

override fun descriptionUpdateInterval(context: Context) =
context.getText(R.string.api_openExchangeRates_descriptionUpdateInterval)

override fun descriptionHint(context: Context) =
context.getText(R.string.api_openExchangeRates_hint)

override val baseUrl = "https://openexchangerates.org/api"

override suspend fun getRates(context: Context?, date: LocalDate?): Result<ExchangeRates, FuelError> {
val apiKey = context?.let { Database(it).getOpenExchangeRatesApiKey() }
if (apiKey.isNullOrBlank())
return Result.error(FuelError.wrap(Exception(context?.getString(R.string.error_no_api_key))))

val endpoint =
if (date != null)
"/historical/" + date.format(DateTimeFormatter.ISO_LOCAL_DATE) + ".json"
else
"/latest.json"

val result = Fuel.get(
baseUrl +
endpoint +
"?app_id=$apiKey" +
"&prettyprint=false" +
"&show_alternative=false"
).awaitResult(
moshiDeserializerOf(
Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.apply {
add(OpenExchangeratesRatesAdapter())
}
.build()
.adapter(ExchangeRates::class.java)
)
).map { rates ->
rates.copy(provider = ApiProvider.OPEN_EXCHANGERATES)
}

if (result.component2()?.response?.statusCode == 401) {
return Result.error(
FuelError.wrap(
Exception(context.getString(R.string.error_invalid_api_key))
)
)
}
return result
}

override suspend fun getTimeline(
context: Context?,
base: Currency,
symbol: Currency,
startDate: LocalDate,
endDate: LocalDate
): Result<Timeline, FuelError> {
return Result.error(FuelError.wrap(Exception(context?.getString(R.string.error_unsupported_timeline))))
}

}
15 changes: 15 additions & 0 deletions app/src/main/kotlin/de/salomax/currencies/repository/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ class Database(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("prefs", MODE_PRIVATE)

private val keyApi = "_api"
private val keyOpenExchangeratesApiKey = "_api_openExchangeratesApiKey"
private val keyTheme = "_theme"
private val keyPureBlackEnabled = "_pureBlackEnabled"
private val keyFeeEnabled = "_feeEnabled"
Expand All @@ -189,6 +190,20 @@ class Database(context: Context) {
}
}

fun setOpenExchangeRatesApiKey(id: String?) {
prefs.apply {
edit().putString(keyOpenExchangeratesApiKey, id).apply()
}
}

fun getOpenExchangeRatesApiKey(): String? {
return prefs.getString(keyOpenExchangeratesApiKey, null)
}

fun getOpenExchangeRatesApiKeyAsync(): LiveData<String?> {
return SharedPreferenceStringLiveData(prefs, keyOpenExchangeratesApiKey, null)
}

/* theme */

fun setTheme(theme: Int) {
Expand Down
Loading

0 comments on commit e0207d3

Please sign in to comment.