diff --git a/app/build.gradle b/app/build.gradle
index f952e32b7..a07bd4547 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -75,17 +75,25 @@ android {
}
dependencies {
- implementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
- kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.14.0'
- implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.3"
implementation "androidx.compose.ui:ui:$compose_version"
- implementation 'io.coil-kt:coil:2.2.1'
- implementation 'io.coil-kt:coil-gif:2.2.1'
- implementation 'io.coil-kt:coil-compose:2.2.1'
+ implementation "androidx.navigation:navigation-compose:2.5.2"
+ implementation "com.google.accompanist:accompanist-navigation-animation:0.27.0"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.5.1'
+
+ implementation "com.google.dagger:dagger:2.42"
+ kapt "com.google.dagger:dagger-compiler:2.42"
+
+ implementation "androidx.room:room-runtime:2.4.3"
+ implementation "androidx.room:room-ktx:2.4.3"
+ kapt "androidx.room:room-compiler:2.4.3"
+
+ implementation 'com.squareup.retrofit2:retrofit:2.9.0'
+ implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
+ implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
+
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cafbf3819..99683d64c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,10 +3,11 @@
xmlns:tools="http://schemas.android.com/tools"
package="space.taran.arkrate">
+
diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyAmount.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyAmount.kt
new file mode 100644
index 000000000..e3dbfcaf9
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/CurrencyAmount.kt
@@ -0,0 +1,6 @@
+package space.taran.arkrate.data
+
+data class CurrencyAmount(
+ val code: String,
+ var amount: Double
+)
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyName.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyName.kt
new file mode 100644
index 000000000..0edf0d617
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/CurrencyName.kt
@@ -0,0 +1,6 @@
+package space.taran.arkrate.data
+
+data class CurrencyName(
+ val code: String,
+ val name: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyRate.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyRate.kt
new file mode 100644
index 000000000..f30043361
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/CurrencyRate.kt
@@ -0,0 +1,6 @@
+package space.taran.arkrate.data
+
+data class CurrencyRate(
+ val code: String,
+ val rate: Double
+)
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyRepo.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyRepo.kt
new file mode 100644
index 000000000..d48665b48
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/CurrencyRepo.kt
@@ -0,0 +1,56 @@
+package space.taran.arkrate.data
+
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import space.taran.arkrate.data.db.CurrencyRateLocalDataSource
+import space.taran.arkrate.data.db.FetchTimestampDataSource
+import space.taran.arkrate.data.network.NetworkStatus
+import space.taran.arkrate.utils.withContextAndLock
+import java.util.concurrent.TimeUnit
+
+abstract class CurrencyRepo(
+ private val local: CurrencyRateLocalDataSource,
+ private val networkStatus: NetworkStatus,
+ private val fetchTimestampDataSource: FetchTimestampDataSource
+) {
+ protected abstract val type: CurrencyType
+ private var currencyRates: List? = null
+ private var updatedTS: Long? = null
+ private val mutex = Mutex()
+
+ suspend fun getCurrencyRate(): List =
+ withContextAndLock(Dispatchers.IO, mutex) {
+ if (!networkStatus.isOnline()) {
+ currencyRates = local.getByType(type)
+ return@withContextAndLock currencyRates!!
+ }
+
+ updatedTS ?: let {
+ updatedTS = fetchTimestampDataSource.getTimestamp(type)
+ }
+
+ if (
+ updatedTS == null ||
+ updatedTS!! + dayInMillis < System.currentTimeMillis()
+ ) {
+ currencyRates = fetchRemote()
+ launch { fetchTimestampDataSource.rememberTimestamp(type) }
+ launch { local.insert(currencyRates!!, type) }
+ updatedTS = System.currentTimeMillis()
+ }
+
+ currencyRates ?: let {
+ currencyRates = local.getByType(type)
+ }
+ Log.d("Test", "${currencyRates!!.sortedBy { it.code }}")
+ return@withContextAndLock currencyRates!!
+ }
+
+ protected abstract suspend fun fetchRemote(): List
+
+ abstract suspend fun getCurrencyName(): List
+
+ private val dayInMillis = TimeUnit.DAYS.toMillis(1)
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyType.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyType.kt
new file mode 100644
index 000000000..094391899
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/CurrencyType.kt
@@ -0,0 +1,5 @@
+package space.taran.arkrate.data
+
+enum class CurrencyType {
+ FIAT, CRYPTO
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/GeneralCurrencyRepo.kt b/app/src/main/java/space/taran/arkrate/data/GeneralCurrencyRepo.kt
new file mode 100644
index 000000000..896247077
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/GeneralCurrencyRepo.kt
@@ -0,0 +1,27 @@
+package space.taran.arkrate.data
+
+import space.taran.arkrate.data.crypto.CryptoCurrencyRepo
+import space.taran.arkrate.data.fiat.FiatCurrencyRepo
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class GeneralCurrencyRepo @Inject constructor(
+ val fiatRepo: FiatCurrencyRepo,
+ val cryptoRepo: CryptoCurrencyRepo
+) {
+ private val currencyRepos = listOf(
+ fiatRepo,
+ cryptoRepo
+ )
+
+ suspend fun getCurrencyRate(): List =
+ currencyRepos.fold(emptyList()) { codeToRate, repo ->
+ codeToRate + repo.getCurrencyRate()
+ }
+
+ suspend fun getCurrencyName(): List =
+ currencyRepos.fold(emptyList()) { currencyName, repo ->
+ currencyName + repo.getCurrencyName()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/assets/AssetsRepo.kt b/app/src/main/java/space/taran/arkrate/data/assets/AssetsRepo.kt
new file mode 100644
index 000000000..80a00d9ae
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/assets/AssetsRepo.kt
@@ -0,0 +1,60 @@
+package space.taran.arkrate.data.assets
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import space.taran.arkrate.data.CurrencyAmount
+import space.taran.arkrate.data.db.AssetsLocalDataSource
+import space.taran.arkrate.utils.replace
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AssetsRepo @Inject constructor(
+ private val local: AssetsLocalDataSource
+) {
+ private var currencyAmountList = listOf()
+ private val currencyAmountFlow =
+ MutableStateFlow>(emptyList())
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ init {
+ scope.launch {
+ currencyAmountList = local.getAll()
+ currencyAmountFlow.emit(currencyAmountList)
+ }
+ }
+
+ fun allCurrencyAmount(): List = currencyAmountList
+
+ fun allCurrencyAmountFlow(): StateFlow> = currencyAmountFlow
+
+ suspend fun setCurrencyAmount(code: String, amount: Double) =
+ withContext(Dispatchers.IO) {
+ currencyAmountList.find {
+ it.code == code
+ }?.let { currencyAmount ->
+ currencyAmountList = currencyAmountList.replace(
+ currencyAmount,
+ currencyAmount.copy(amount = amount)
+ )
+ } ?: let {
+ currencyAmountList =
+ currencyAmountList + CurrencyAmount(code, amount)
+ }
+ currencyAmountFlow.emit(currencyAmountList.toList())
+ local.insert(CurrencyAmount(code, amount))
+ }
+
+ suspend fun removeCurrency(code: String) = withContext(Dispatchers.IO) {
+ currencyAmountList.find { it.code == code }?.let {
+ currencyAmountList = currencyAmountList - it
+ }
+ currencyAmountFlow.emit(currencyAmountList)
+ local.delete(code)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/crypto/CryptoAPI.kt b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoAPI.kt
new file mode 100644
index 000000000..a4c2ca733
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoAPI.kt
@@ -0,0 +1,8 @@
+package space.taran.arkrate.data.crypto
+
+import retrofit2.http.GET
+
+interface CryptoAPI {
+ @GET("/ARK-Builders/ark-exchange-rates/main/crypto-rates.json")
+ suspend fun getCryptoRates(): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/crypto/CryptoCurrencyRepo.kt b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoCurrencyRepo.kt
new file mode 100644
index 000000000..91da7c321
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoCurrencyRepo.kt
@@ -0,0 +1,39 @@
+package space.taran.arkrate.data.crypto
+
+import space.taran.arkrate.data.CurrencyName
+import space.taran.arkrate.data.CurrencyRate
+import space.taran.arkrate.data.CurrencyRepo
+import space.taran.arkrate.data.CurrencyType
+import space.taran.arkrate.data.network.NetworkStatus
+import space.taran.arkrate.data.db.CurrencyRateLocalDataSource
+import space.taran.arkrate.data.db.FetchTimestampDataSource
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class CryptoCurrencyRepo @Inject constructor(
+ private val cryptoAPI: CryptoAPI,
+ private val local: CurrencyRateLocalDataSource,
+ private val networkStatus: NetworkStatus,
+ private val fetchTimestampDataSource: FetchTimestampDataSource
+) : CurrencyRepo(local, networkStatus, fetchTimestampDataSource) {
+ override val type = CurrencyType.CRYPTO
+
+ override suspend fun fetchRemote(): List =
+ cryptoAPI.getCryptoRates().findUSDTPairs()
+
+ override suspend fun getCurrencyName(): List =
+ getCurrencyRate().map {
+ CurrencyName(it.code, name = "")
+ }
+
+ // api returns pairs like ETHBTC, ETHBNB, ETHTRX, ETHUSDT
+ // we only take USDT pairs
+ private fun List.findUSDTPairs() =
+ mapNotNull { (code, price) ->
+ if (code.takeLast(4) == "USDT") {
+ CurrencyRate(code.dropLast(4), price)
+ } else
+ null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/crypto/CryptoRatesResponse.kt b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoRatesResponse.kt
new file mode 100644
index 000000000..9812e0ec0
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoRatesResponse.kt
@@ -0,0 +1,6 @@
+package space.taran.arkrate.data.crypto
+
+data class CryptoRateResponse(
+ val symbol: String,
+ val price: Double
+)
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt b/app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt
new file mode 100644
index 000000000..78b0212d8
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt
@@ -0,0 +1,41 @@
+package space.taran.arkrate.data.db
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import space.taran.arkrate.data.CurrencyAmount
+import javax.inject.Inject
+
+@Entity
+data class RoomCurrencyAmount(
+ @PrimaryKey
+ val code: String,
+ val amount: Double
+)
+
+@Dao
+interface AssetsDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(currencyAmount: RoomCurrencyAmount)
+
+ @Query("SELECT * FROM RoomCurrencyAmount")
+ suspend fun getAll(): List
+
+ @Query("DELETE FROM RoomCurrencyAmount where code = :code")
+ suspend fun delete(code: String)
+}
+
+class AssetsLocalDataSource @Inject constructor(val dao: AssetsDao) {
+ suspend fun insert(currencyAmount: CurrencyAmount) =
+ dao.insert(currencyAmount.toRoom())
+
+ suspend fun getAll() = dao.getAll().map { it.toCurrencyAmount() }
+
+ suspend fun delete(code: String) = dao.delete(code)
+}
+
+private fun RoomCurrencyAmount.toCurrencyAmount() = CurrencyAmount(code, amount)
+private fun CurrencyAmount.toRoom() = RoomCurrencyAmount(code, amount)
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/db/CurrencyRateDao.kt b/app/src/main/java/space/taran/arkrate/data/db/CurrencyRateDao.kt
new file mode 100644
index 000000000..114942154
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/db/CurrencyRateDao.kt
@@ -0,0 +1,42 @@
+package space.taran.arkrate.data.db
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import space.taran.arkrate.data.CurrencyRate
+import space.taran.arkrate.data.CurrencyType
+import javax.inject.Inject
+
+@Entity
+data class RoomCurrencyRate(
+ @PrimaryKey
+ val code: String,
+ val currencyType: String,
+ val rate: Double
+)
+
+@Dao
+interface CurrencyRateDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(currencyRate: List)
+
+ @Query("SELECT * FROM RoomCurrencyRate where currencyType = :currencyType")
+ suspend fun getByType(currencyType: String): List
+}
+
+class CurrencyRateLocalDataSource @Inject constructor(val dao: CurrencyRateDao) {
+ suspend fun insert(
+ currencyRate: List,
+ currencyType: CurrencyType
+ ) = dao.insert(currencyRate.map { it.toRoom(currencyType) })
+
+ suspend fun getByType(currencyType: CurrencyType) =
+ dao.getByType(currencyType.name).map { it.toCurrencyRate() }
+}
+
+private fun RoomCurrencyRate.toCurrencyRate() = CurrencyRate(code, rate)
+private fun CurrencyRate.toRoom(currencyType: CurrencyType) =
+ RoomCurrencyRate(code, currencyType.name, rate)
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/db/Database.kt b/app/src/main/java/space/taran/arkrate/data/db/Database.kt
new file mode 100644
index 000000000..ce7bcd460
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/db/Database.kt
@@ -0,0 +1,22 @@
+package space.taran.arkrate.data.db
+
+import androidx.room.RoomDatabase
+
+@androidx.room.Database(
+ entities = [
+ RoomCurrencyAmount::class,
+ RoomCurrencyRate::class,
+ RoomFetchTimestamp::class
+ ],
+ version = 1,
+ exportSchema = false
+)
+abstract class Database : RoomDatabase() {
+ abstract fun assetsDao(): AssetsDao
+ abstract fun rateDao(): CurrencyRateDao
+ abstract fun fetchTimestampDao(): FetchTimestampDao
+
+ companion object {
+ const val DB_NAME = "arkrate.db"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/db/FetchTimestampDao.kt b/app/src/main/java/space/taran/arkrate/data/db/FetchTimestampDao.kt
new file mode 100644
index 000000000..e0cba28b3
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/db/FetchTimestampDao.kt
@@ -0,0 +1,35 @@
+package space.taran.arkrate.data.db
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import space.taran.arkrate.data.CurrencyType
+import javax.inject.Inject
+
+@Entity
+data class RoomFetchTimestamp(
+ @PrimaryKey
+ val currencyType: String,
+ val timestamp: Long
+)
+
+@Dao
+interface FetchTimestampDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(fetchTimestamp: RoomFetchTimestamp)
+
+ @Query("SELECT * FROM RoomFetchTimestamp where currencyType = :currencyType")
+ suspend fun getTimestamp(currencyType: String): RoomFetchTimestamp?
+}
+
+class FetchTimestampDataSource @Inject constructor(private val dao: FetchTimestampDao) {
+ suspend fun rememberTimestamp(currencyType: CurrencyType) =
+ dao.insert(RoomFetchTimestamp(currencyType.name, System.currentTimeMillis()))
+
+ suspend fun getTimestamp(currencyType: CurrencyType) =
+ dao.getTimestamp(currencyType.name)?.timestamp
+}
+
diff --git a/app/src/main/java/space/taran/arkrate/data/fiat/FiatAPI.kt b/app/src/main/java/space/taran/arkrate/data/fiat/FiatAPI.kt
new file mode 100644
index 000000000..9f6024b1f
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/fiat/FiatAPI.kt
@@ -0,0 +1,8 @@
+package space.taran.arkrate.data.fiat
+
+import retrofit2.http.GET
+
+interface FiatAPI {
+ @GET("/ARK-Builders/ark-exchange-rates/main/fiat-rates.json")
+ suspend fun get(): FiatRateResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/fiat/FiatCurrencyRepo.kt b/app/src/main/java/space/taran/arkrate/data/fiat/FiatCurrencyRepo.kt
new file mode 100644
index 000000000..4727de658
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/fiat/FiatCurrencyRepo.kt
@@ -0,0 +1,204 @@
+package space.taran.arkrate.data.fiat
+
+import space.taran.arkrate.data.CurrencyName
+import space.taran.arkrate.data.CurrencyRate
+import space.taran.arkrate.data.CurrencyRepo
+import space.taran.arkrate.data.CurrencyType
+import space.taran.arkrate.data.network.NetworkStatus
+import space.taran.arkrate.data.db.CurrencyRateLocalDataSource
+import space.taran.arkrate.data.db.FetchTimestampDataSource
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class FiatCurrencyRepo @Inject constructor(
+ private val fiatAPI: FiatAPI,
+ private val local: CurrencyRateLocalDataSource,
+ private val networkStatus: NetworkStatus,
+ private val fetchTimestampDataSource: FetchTimestampDataSource
+) : CurrencyRepo(local, networkStatus, fetchTimestampDataSource) {
+ override val type = CurrencyType.FIAT
+
+ override suspend fun fetchRemote(): List =
+ fiatAPI.get().rates.map { (code, rate) ->
+ CurrencyRate(code, 1.0 / rate)
+ }
+
+ override suspend fun getCurrencyName(): List =
+ getCurrencyRate().map {
+ CurrencyName(it.code, fiatCodeToCurrency[it.code]!!)
+ }
+}
+
+private val fiatCodeToCurrency = mutableMapOf(
+ "AED" to "United Arab Emirates Dirham",
+ "AFN" to "Afghan Afghani",
+ "ALL" to "Albanian Lek",
+ "AMD" to "Armenian Dram",
+ "ANG" to "Netherlands Antillean Guilder",
+ "AOA" to "Angolan Kwanza",
+ "ARS" to "Argentine Peso",
+ "AUD" to "Australian Dollar",
+ "AWG" to "Aruban Florin",
+ "AZN" to "Azerbaijani Manat",
+ "BAM" to "Bosnia-Herzegovina Convertible Mark",
+ "BBD" to "Barbadian Dollar",
+ "BDT" to "Bangladeshi Taka",
+ "BGN" to "Bulgarian Lev",
+ "BHD" to "Bahraini Dinar",
+ "BIF" to "Burundian Franc",
+ "BMD" to "Bermudan Dollar",
+ "BND" to "Brunei Dollar",
+ "BOB" to "Bolivian Boliviano",
+ "BRL" to "Brazilian Real",
+ "BSD" to "Bahamian Dollar",
+ "BTC" to "Bitcoin",
+ "BTN" to "Bhutanese Ngultrum",
+ "BWP" to "Botswanan Pula",
+ "BYN" to "Belarusian Ruble",
+ "BZD" to "Belize Dollar",
+ "CAD" to "Canadian Dollar",
+ "CDF" to "Congolese Franc",
+ "CHF" to "Swiss Franc",
+ "CLF" to "Chilean Unit of Account (UF)",
+ "CLP" to "Chilean Peso",
+ "CNH" to "Chinese Yuan (Offshore)",
+ "CNY" to "Chinese Yuan",
+ "COP" to "Colombian Peso",
+ "CRC" to "Costa Rican Colón",
+ "CUC" to "Cuban Convertible Peso",
+ "CUP" to "Cuban Peso",
+ "CVE" to "Cape Verdean Escudo",
+ "CZK" to "Czech Republic Koruna",
+ "DJF" to "Djiboutian Franc",
+ "DKK" to "Danish Krone",
+ "DOP" to "Dominican Peso",
+ "DZD" to "Algerian Dinar",
+ "EGP" to "Egyptian Pound",
+ "ERN" to "Eritrean Nakfa",
+ "ETB" to "Ethiopian Birr",
+ "EUR" to "Euro",
+ "FJD" to "Fijian Dollar",
+ "FKP" to "Falkland Islands Pound",
+ "GBP" to "British Pound Sterling",
+ "GEL" to "Georgian Lari",
+ "GGP" to "Guernsey Pound",
+ "GHS" to "Ghanaian Cedi",
+ "GIP" to "Gibraltar Pound",
+ "GMD" to "Gambian Dalasi",
+ "GNF" to "Guinean Franc",
+ "GTQ" to "Guatemalan Quetzal",
+ "GYD" to "Guyanaese Dollar",
+ "HKD" to "Hong Kong Dollar",
+ "HNL" to "Honduran Lempira",
+ "HRK" to "Croatian Kuna",
+ "HTG" to "Haitian Gourde",
+ "HUF" to "Hungarian Forint",
+ "IDR" to "Indonesian Rupiah",
+ "ILS" to "Israeli New Sheqel",
+ "IMP" to "Manx pound",
+ "INR" to "Indian Rupee",
+ "IQD" to "Iraqi Dinar",
+ "IRR" to "Iranian Rial",
+ "ISK" to "Icelandic Króna",
+ "JEP" to "Jersey Pound",
+ "JMD" to "Jamaican Dollar",
+ "JOD" to "Jordanian Dinar",
+ "JPY" to "Japanese Yen",
+ "KES" to "Kenyan Shilling",
+ "KGS" to "Kyrgystani Som",
+ "KHR" to "Cambodian Riel",
+ "KMF" to "Comorian Franc",
+ "KPW" to "North Korean Won",
+ "KRW" to "South Korean Won",
+ "KWD" to "Kuwaiti Dinar",
+ "KYD" to "Cayman Islands Dollar",
+ "KZT" to "Kazakhstani Tenge",
+ "LAK" to "Laotian Kip",
+ "LBP" to "Lebanese Pound",
+ "LKR" to "Sri Lankan Rupee",
+ "LRD" to "Liberian Dollar",
+ "LSL" to "Lesotho Loti",
+ "LYD" to "Libyan Dinar",
+ "MAD" to "Moroccan Dirham",
+ "MDL" to "Moldovan Leu",
+ "MGA" to "Malagasy Ariary",
+ "MKD" to "Macedonian Denar",
+ "MMK" to "Myanma Kyat",
+ "MNT" to "Mongolian Tugrik",
+ "MOP" to "Macanese Pataca",
+ "MRU" to "Mauritanian Ouguiya",
+ "MUR" to "Mauritian Rupee",
+ "MVR" to "Maldivian Rufiyaa",
+ "MWK" to "Malawian Kwacha",
+ "MXN" to "Mexican Peso",
+ "MYR" to "Malaysian Ringgit",
+ "MZN" to "Mozambican Metical",
+ "NAD" to "Namibian Dollar",
+ "NGN" to "Nigerian Naira",
+ "NIO" to "Nicaraguan Córdoba",
+ "NOK" to "Norwegian Krone",
+ "NPR" to "Nepalese Rupee",
+ "NZD" to "New Zealand Dollar",
+ "OMR" to "Omani Rial",
+ "PAB" to "Panamanian Balboa",
+ "PEN" to "Peruvian Nuevo Sol",
+ "PGK" to "Papua New Guinean Kina",
+ "PHP" to "Philippine Peso",
+ "PKR" to "Pakistani Rupee",
+ "PLN" to "Polish Zloty",
+ "PYG" to "Paraguayan Guarani",
+ "QAR" to "Qatari Rial",
+ "RON" to "Romanian Leu",
+ "RSD" to "Serbian Dinar",
+ "RUB" to "Russian Ruble",
+ "RWF" to "Rwandan Franc",
+ "SAR" to "Saudi Riyal",
+ "SBD" to "Solomon Islands Dollar",
+ "SCR" to "Seychellois Rupee",
+ "SDG" to "Sudanese Pound",
+ "SEK" to "Swedish Krona",
+ "SGD" to "Singapore Dollar",
+ "SHP" to "Saint Helena Pound",
+ "SLL" to "Sierra Leonean Leone",
+ "SOS" to "Somali Shilling",
+ "SRD" to "Surinamese Dollar",
+ "SSP" to "South Sudanese Pound",
+ "STD" to "São Tomé and Príncipe Dobra (pre-2018)",
+ "STN" to "São Tomé and Príncipe Dobra",
+ "SVC" to "Salvadoran Colón",
+ "SYP" to "Syrian Pound",
+ "SZL" to "Swazi Lilangeni",
+ "THB" to "Thai Baht",
+ "TJS" to "Tajikistani Somoni",
+ "TMT" to "Turkmenistani Manat",
+ "TND" to "Tunisian Dinar",
+ "TOP" to "Tongan Pa'anga",
+ "TRY" to "Turkish Lira",
+ "TTD" to "Trinidad and Tobago Dollar",
+ "TWD" to "New Taiwan Dollar",
+ "TZS" to "Tanzanian Shilling",
+ "UAH" to "Ukrainian Hryvnia",
+ "UGX" to "Ugandan Shilling",
+ "USD" to "United States Dollar",
+ "UYU" to "Uruguayan Peso",
+ "UZS" to "Uzbekistan Som",
+ "VEF" to "Venezuelan Bolívar Fuerte (Old)",
+ "VES" to "Venezuelan Bolívar Soberano",
+ "VND" to "Vietnamese Dong",
+ "VUV" to "Vanuatu Vatu",
+ "WST" to "Samoan Tala",
+ "XAF" to "CFA Franc BEAC",
+ "XAG" to "Silver Ounce",
+ "XAU" to "Gold Ounce",
+ "XCD" to "East Caribbean Dollar",
+ "XDR" to "Special Drawing Rights",
+ "XOF" to "CFA Franc BCEAO",
+ "XPD" to "Palladium Ounce",
+ "XPF" to "CFP Franc",
+ "XPT" to "Platinum Ounce",
+ "YER" to "Yemeni Rial",
+ "ZAR" to "South African Rand",
+ "ZMW" to "Zambian Kwacha",
+ "ZWL" to "Zimbabwean Dollar"
+)
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/fiat/FiatRateResponse.kt b/app/src/main/java/space/taran/arkrate/data/fiat/FiatRateResponse.kt
new file mode 100644
index 000000000..81d35079a
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/fiat/FiatRateResponse.kt
@@ -0,0 +1,6 @@
+package space.taran.arkrate.data.fiat
+
+data class FiatRateResponse(
+ val timestamp: Long,
+ val rates: Map
+)
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/network/NetworkStatus.kt b/app/src/main/java/space/taran/arkrate/data/network/NetworkStatus.kt
new file mode 100644
index 000000000..2ebc5166a
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/network/NetworkStatus.kt
@@ -0,0 +1,30 @@
+package space.taran.arkrate.data.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.os.Build
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class NetworkStatus @Inject constructor(private val context: Context) {
+ private val cm =
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ fun isOnline(): Boolean {
+ val network: Network = cm.activeNetwork ?: return false
+ val networkCapabilities: NetworkCapabilities =
+ cm.getNetworkCapabilities(network) ?: return false
+
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ } else {
+ networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/network/OkHttpClientBuilder.kt b/app/src/main/java/space/taran/arkrate/data/network/OkHttpClientBuilder.kt
new file mode 100644
index 000000000..cfbba7e5b
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/network/OkHttpClientBuilder.kt
@@ -0,0 +1,36 @@
+package space.taran.arkrate.data.network
+
+import android.content.Context
+import android.webkit.WebSettings
+import okhttp3.CipherSuite
+import okhttp3.ConnectionSpec
+import okhttp3.OkHttpClient
+import okhttp3.TlsVersion
+import okhttp3.logging.HttpLoggingInterceptor
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
+import java.util.Collections
+import javax.inject.Inject
+import javax.inject.Singleton
+import javax.net.ssl.SSLContext
+import javax.net.ssl.X509TrustManager
+
+@Singleton
+class OkHttpClientBuilder @Inject constructor(val context: Context) {
+ fun build(): OkHttpClient {
+ val agent = WebSettings.getDefaultUserAgent(context)
+
+ val client = OkHttpClient.Builder()
+ .addNetworkInterceptor { chain ->
+ chain.proceed(
+ chain.request()
+ .newBuilder()
+ .header("User-Agent", agent)
+ .build()
+ )
+ }
+ .build()
+
+ return client
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/di/AppComponent.kt b/app/src/main/java/space/taran/arkrate/di/AppComponent.kt
new file mode 100644
index 000000000..327c29476
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/di/AppComponent.kt
@@ -0,0 +1,37 @@
+package space.taran.arkrate.di
+
+import android.app.Application
+import android.content.Context
+import dagger.BindsInstance
+import dagger.Component
+import space.taran.arkrate.data.GeneralCurrencyRepo
+import space.taran.arkrate.data.assets.AssetsRepo
+import space.taran.arkrate.di.module.ApiModule
+import space.taran.arkrate.di.module.DBModule
+import space.taran.arkrate.presentation.summary.SummaryViewModelFactory
+import space.taran.arkrate.presentation.addcurrency.AddCurrencyViewModelFactory
+import space.taran.arkrate.presentation.assets.AssetsViewModelFactory
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+ modules = [
+ ApiModule::class,
+ DBModule::class
+ ]
+)
+interface AppComponent {
+ fun summaryViewModelFactory(): SummaryViewModelFactory
+ fun assetsVMFactory(): AssetsViewModelFactory
+ fun addCurrencyVMFactory(): AddCurrencyViewModelFactory
+ fun generalCurrencyRepo(): GeneralCurrencyRepo
+ fun assetsRepo(): AssetsRepo
+
+ @Component.Factory
+ interface Factory {
+ fun create(
+ @BindsInstance application: Application,
+ @BindsInstance context: Context
+ ): AppComponent
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/di/DIManager.kt b/app/src/main/java/space/taran/arkrate/di/DIManager.kt
new file mode 100644
index 000000000..d264925cc
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/di/DIManager.kt
@@ -0,0 +1,12 @@
+package space.taran.arkrate.di
+
+import android.app.Application
+
+object DIManager {
+ lateinit var component: AppComponent
+ private set
+
+ fun init(app: Application) {
+ component = DaggerAppComponent.factory().create(app, app.applicationContext)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/di/module/ApiModule.kt b/app/src/main/java/space/taran/arkrate/di/module/ApiModule.kt
new file mode 100644
index 000000000..a22ce3bd6
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/di/module/ApiModule.kt
@@ -0,0 +1,44 @@
+package space.taran.arkrate.di.module
+
+import com.google.gson.GsonBuilder
+import dagger.Module
+import dagger.Provides
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import space.taran.arkrate.data.crypto.CryptoAPI
+import space.taran.arkrate.data.fiat.FiatAPI
+import space.taran.arkrate.data.network.OkHttpClientBuilder
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Module
+class ApiModule {
+ @Singleton
+ @Provides
+ fun cryptoAPI(clientBuilder: OkHttpClientBuilder): CryptoAPI {
+ val httpClient = clientBuilder.build()
+ val gson = GsonBuilder().create()
+
+ return Retrofit.Builder()
+ .baseUrl("https://raw.githubusercontent.com")
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .client(httpClient)
+ .build()
+ .create(CryptoAPI::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun fiatAPI(clientBuilder: OkHttpClientBuilder): FiatAPI {
+ val httpClient = clientBuilder.build()
+ val gson = GsonBuilder().create()
+
+ return Retrofit.Builder()
+ .baseUrl("https://raw.githubusercontent.com")
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .client(httpClient)
+ .build()
+ .create(FiatAPI::class.java)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/di/module/DBModule.kt b/app/src/main/java/space/taran/arkrate/di/module/DBModule.kt
new file mode 100644
index 000000000..f23a1d8c9
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/di/module/DBModule.kt
@@ -0,0 +1,30 @@
+package space.taran.arkrate.di.module
+
+import android.app.Application
+import androidx.room.Room
+import dagger.Module
+import dagger.Provides
+import space.taran.arkrate.data.db.Database
+import space.taran.arkrate.data.db.Database.Companion.DB_NAME
+import javax.inject.Singleton
+
+@Module
+class DBModule {
+
+ @Singleton
+ @Provides
+ fun database(app: Application): Database {
+ return Room.databaseBuilder(app, Database::class.java, DB_NAME)
+ .fallbackToDestructiveMigration()
+ .build()
+ }
+
+ @Provides
+ fun assetsDao(db: Database) = db.assetsDao()
+
+ @Provides
+ fun rateDao(db: Database) = db.rateDao()
+
+ @Provides
+ fun fetchTimestampDao(db: Database) = db.fetchTimestampDao()
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/network/crypto.kt b/app/src/main/java/space/taran/arkrate/network/crypto.kt
deleted file mode 100644
index 89671e50d..000000000
--- a/app/src/main/java/space/taran/arkrate/network/crypto.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package space.taran.arkrate.network
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-import com.squareup.moshi.Moshi
-import com.squareup.moshi.Types
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import java.io.File
-
-@JsonClass(generateAdapter = true)
-data class BinancePrizeItem(
- @Json(name = "price")
- val price: String,
- @Json(name = "symbol")
- val symbol: String
-)
-
-@JsonClass(generateAdapter = true)
-data class BinancePrizeTimestamp(
- val timestamp: Int,
- val binancePrize: Map
-)
-
-class Crypto {
- private fun getCryptoPrice(): Map {
-
- val client = OkHttpClient()
- val request = Request.Builder()
- .url("https://api.binance.com/api/v3/ticker/price")
- .build()
-
- val response = client.newCall(request).execute().body!!.string()
- val moshi: Moshi = Moshi.Builder().build()
- val jsonAdapter = moshi.adapter>(
- Types.newParameterizedType(
- List::class.java,
- BinancePrizeItem::class.java
- )
- )
-
- val json = jsonAdapter.fromJson(response)
- val result = mutableMapOf()
- json!!.forEach {
- result[it.symbol] = it.price.toDouble()
- }
- return result
- }
-
- fun getCryptoPriceWithCache(filePath: String): Map {
- val file = File((File(filePath).parent ?: "") + "/CryptoCache.json")
- val moshi: Moshi = Moshi.Builder().build()
- val jsonAdapter = moshi.adapter(BinancePrizeTimestamp::class.java)
- if (!file.canWrite()) {
- val result = BinancePrizeTimestamp(
- timestamp = (System.currentTimeMillis() / 1000).toInt(),
- getCryptoPrice()
- )
- file.writeText(jsonAdapter.toJson(result))
- return result.binancePrize
- }
- val origin = file.readText()
- val json = jsonAdapter.fromJson(origin)
- if ((json?.timestamp ?: 0) + 86400 < System.currentTimeMillis() / 1000) {
- val result = BinancePrizeTimestamp(
- timestamp = (System.currentTimeMillis() / 1000).toInt(),
- getCryptoPrice()
- )
- file.writeText(jsonAdapter.toJson(result))
- return result.binancePrize
- }
- return json!!.binancePrize
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/network/currencies.kt b/app/src/main/java/space/taran/arkrate/network/currencies.kt
deleted file mode 100644
index f591b7202..000000000
--- a/app/src/main/java/space/taran/arkrate/network/currencies.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-package space.taran.arkrate.network
-
-
-object currencies {
- val currencies: Map
- get() {
- val result = mutableMapOf(
- "AED" to "United Arab Emirates Dirham",
- "AFN" to "Afghan Afghani",
- "ALL" to "Albanian Lek",
- "AMD" to "Armenian Dram",
- "ANG" to "Netherlands Antillean Guilder",
- "AOA" to "Angolan Kwanza",
- "ARS" to "Argentine Peso",
- "AUD" to "Australian Dollar",
- "AWG" to "Aruban Florin",
- "AZN" to "Azerbaijani Manat",
- "BAM" to "Bosnia-Herzegovina Convertible Mark",
- "BBD" to "Barbadian Dollar",
- "BDT" to "Bangladeshi Taka",
- "BGN" to "Bulgarian Lev",
- "BHD" to "Bahraini Dinar",
- "BIF" to "Burundian Franc",
- "BMD" to "Bermudan Dollar",
- "BND" to "Brunei Dollar",
- "BOB" to "Bolivian Boliviano",
- "BRL" to "Brazilian Real",
- "BSD" to "Bahamian Dollar",
- "BTC" to "Bitcoin",
- "BTN" to "Bhutanese Ngultrum",
- "BWP" to "Botswanan Pula",
- "BYN" to "Belarusian Ruble",
- "BZD" to "Belize Dollar",
- "CAD" to "Canadian Dollar",
- "CDF" to "Congolese Franc",
- "CHF" to "Swiss Franc",
- "CLF" to "Chilean Unit of Account (UF)",
- "CLP" to "Chilean Peso",
- "CNH" to "Chinese Yuan (Offshore)",
- "CNY" to "Chinese Yuan",
- "COP" to "Colombian Peso",
- "CRC" to "Costa Rican Colón",
- "CUC" to "Cuban Convertible Peso",
- "CUP" to "Cuban Peso",
- "CVE" to "Cape Verdean Escudo",
- "CZK" to "Czech Republic Koruna",
- "DJF" to "Djiboutian Franc",
- "DKK" to "Danish Krone",
- "DOP" to "Dominican Peso",
- "DZD" to "Algerian Dinar",
- "EGP" to "Egyptian Pound",
- "ERN" to "Eritrean Nakfa",
- "ETB" to "Ethiopian Birr",
- "EUR" to "Euro",
- "FJD" to "Fijian Dollar",
- "FKP" to "Falkland Islands Pound",
- "GBP" to "British Pound Sterling",
- "GEL" to "Georgian Lari",
- "GGP" to "Guernsey Pound",
- "GHS" to "Ghanaian Cedi",
- "GIP" to "Gibraltar Pound",
- "GMD" to "Gambian Dalasi",
- "GNF" to "Guinean Franc",
- "GTQ" to "Guatemalan Quetzal",
- "GYD" to "Guyanaese Dollar",
- "HKD" to "Hong Kong Dollar",
- "HNL" to "Honduran Lempira",
- "HRK" to "Croatian Kuna",
- "HTG" to "Haitian Gourde",
- "HUF" to "Hungarian Forint",
- "IDR" to "Indonesian Rupiah",
- "ILS" to "Israeli New Sheqel",
- "IMP" to "Manx pound",
- "INR" to "Indian Rupee",
- "IQD" to "Iraqi Dinar",
- "IRR" to "Iranian Rial",
- "ISK" to "Icelandic Króna",
- "JEP" to "Jersey Pound",
- "JMD" to "Jamaican Dollar",
- "JOD" to "Jordanian Dinar",
- "JPY" to "Japanese Yen",
- "KES" to "Kenyan Shilling",
- "KGS" to "Kyrgystani Som",
- "KHR" to "Cambodian Riel",
- "KMF" to "Comorian Franc",
- "KPW" to "North Korean Won",
- "KRW" to "South Korean Won",
- "KWD" to "Kuwaiti Dinar",
- "KYD" to "Cayman Islands Dollar",
- "KZT" to "Kazakhstani Tenge",
- "LAK" to "Laotian Kip",
- "LBP" to "Lebanese Pound",
- "LKR" to "Sri Lankan Rupee",
- "LRD" to "Liberian Dollar",
- "LSL" to "Lesotho Loti",
- "LYD" to "Libyan Dinar",
- "MAD" to "Moroccan Dirham",
- "MDL" to "Moldovan Leu",
- "MGA" to "Malagasy Ariary",
- "MKD" to "Macedonian Denar",
- "MMK" to "Myanma Kyat",
- "MNT" to "Mongolian Tugrik",
- "MOP" to "Macanese Pataca",
- "MRU" to "Mauritanian Ouguiya",
- "MUR" to "Mauritian Rupee",
- "MVR" to "Maldivian Rufiyaa",
- "MWK" to "Malawian Kwacha",
- "MXN" to "Mexican Peso",
- "MYR" to "Malaysian Ringgit",
- "MZN" to "Mozambican Metical",
- "NAD" to "Namibian Dollar",
- "NGN" to "Nigerian Naira",
- "NIO" to "Nicaraguan Córdoba",
- "NOK" to "Norwegian Krone",
- "NPR" to "Nepalese Rupee",
- "NZD" to "New Zealand Dollar",
- "OMR" to "Omani Rial",
- "PAB" to "Panamanian Balboa",
- "PEN" to "Peruvian Nuevo Sol",
- "PGK" to "Papua New Guinean Kina",
- "PHP" to "Philippine Peso",
- "PKR" to "Pakistani Rupee",
- "PLN" to "Polish Zloty",
- "PYG" to "Paraguayan Guarani",
- "QAR" to "Qatari Rial",
- "RON" to "Romanian Leu",
- "RSD" to "Serbian Dinar",
- "RUB" to "Russian Ruble",
- "RWF" to "Rwandan Franc",
- "SAR" to "Saudi Riyal",
- "SBD" to "Solomon Islands Dollar",
- "SCR" to "Seychellois Rupee",
- "SDG" to "Sudanese Pound",
- "SEK" to "Swedish Krona",
- "SGD" to "Singapore Dollar",
- "SHP" to "Saint Helena Pound",
- "SLL" to "Sierra Leonean Leone",
- "SOS" to "Somali Shilling",
- "SRD" to "Surinamese Dollar",
- "SSP" to "South Sudanese Pound",
- "STD" to "São Tomé and Príncipe Dobra (pre-2018)",
- "STN" to "São Tomé and Príncipe Dobra",
- "SVC" to "Salvadoran Colón",
- "SYP" to "Syrian Pound",
- "SZL" to "Swazi Lilangeni",
- "THB" to "Thai Baht",
- "TJS" to "Tajikistani Somoni",
- "TMT" to "Turkmenistani Manat",
- "TND" to "Tunisian Dinar",
- "TOP" to "Tongan Pa'anga",
- "TRY" to "Turkish Lira",
- "TTD" to "Trinidad and Tobago Dollar",
- "TWD" to "New Taiwan Dollar",
- "TZS" to "Tanzanian Shilling",
- "UAH" to "Ukrainian Hryvnia",
- "UGX" to "Ugandan Shilling",
- "USD" to "United States Dollar",
- "UYU" to "Uruguayan Peso",
- "UZS" to "Uzbekistan Som",
- "VEF" to "Venezuelan Bolívar Fuerte (Old)",
- "VES" to "Venezuelan Bolívar Soberano",
- "VND" to "Vietnamese Dong",
- "VUV" to "Vanuatu Vatu",
- "WST" to "Samoan Tala",
- "XAF" to "CFA Franc BEAC",
- "XAG" to "Silver Ounce",
- "XAU" to "Gold Ounce",
- "XCD" to "East Caribbean Dollar",
- "XDR" to "Special Drawing Rights",
- "XOF" to "CFA Franc BCEAO",
- "XPD" to "Palladium Ounce",
- "XPF" to "CFP Franc",
- "XPT" to "Platinum Ounce",
- "YER" to "Yemeni Rial",
- "ZAR" to "South African Rand",
- "ZMW" to "Zambian Kwacha",
- "ZWL" to "Zimbabwean Dollar"
- )
-
- return result
- }
-
- fun get(filePath: String): Map {
- val result = currencies.toMutableMap()
- Crypto().getCryptoPriceWithCache(filePath).keys.forEach {
- if (it.takeLast(4) == "USDT") {
- result[it.dropLast(4)] = ""
- }
- }
- return result
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/network/rates.kt b/app/src/main/java/space/taran/arkrate/network/rates.kt
deleted file mode 100644
index 276ca1315..000000000
--- a/app/src/main/java/space/taran/arkrate/network/rates.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package space.taran.arkrate.network
-
-import com.squareup.moshi.JsonClass
-import com.squareup.moshi.Moshi
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import java.io.File
-
-@JsonClass(generateAdapter = true)
-data class rates(
- val base: String,
- val disclaimer: String,
- val license: String,
- val rates: Map,
- val timestamp: Int
-)
-
-fun getAllRates(): rates {
- val client = OkHttpClient()
- val request = Request.Builder()
- .url("https://raw.githubusercontent.com/ARK-Builders/ark-exchange-rates/main/rates.json")
- .build()
-
- val response = client.newCall(request).execute().body!!.string()
-
- val moshi: Moshi = Moshi.Builder().build()
- val jsonAdapter = moshi.adapter(rates::class.java)
- val json = jsonAdapter.fromJson(response)
-
- return json!!
-}
-
-fun getAllRatesWithCache(filePath: String): Map {
- val file = File((File(filePath).parent ?: "") + "/rateCache.json")
- val moshi: Moshi = Moshi.Builder().build()
- val jsonAdapter = moshi.adapter(rates::class.java)
- if (!file.canWrite()) {
- val result = getAllRates()
- file.writeText(jsonAdapter.toJson(result))
- return result.rates
- }
- val origin = file.readText()
- val json = jsonAdapter.fromJson(origin)
- if ((json?.timestamp ?: 0) + 86400 < System.currentTimeMillis() / 1000) {
- val result = getAllRates()
- file.writeText(jsonAdapter.toJson(result))
- return result.rates
- }
- return json!!.rates
-}
diff --git a/app/src/main/java/space/taran/arkrate/App.kt b/app/src/main/java/space/taran/arkrate/presentation/App.kt
similarity index 87%
rename from app/src/main/java/space/taran/arkrate/App.kt
rename to app/src/main/java/space/taran/arkrate/presentation/App.kt
index 238e28d5f..f1b3e626d 100644
--- a/app/src/main/java/space/taran/arkrate/App.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/App.kt
@@ -1,4 +1,4 @@
-package space.taran.arkrate
+package space.taran.arkrate.presentation
import android.app.Application
import kotlinx.coroutines.CoroutineScope
@@ -9,6 +9,9 @@ import org.acra.config.httpSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
+import space.taran.arkrate.BuildConfig
+import space.taran.arkrate.R
+import space.taran.arkrate.di.DIManager
import space.taran.arkrate.utils.Config
class App: Application() {
@@ -16,6 +19,7 @@ class App: Application() {
override fun onCreate() {
super.onCreate()
initAcra()
+ DIManager.init(this)
}
private fun initAcra() = CoroutineScope(Dispatchers.IO).launch {
diff --git a/app/src/main/java/space/taran/arkrate/MainActivity.kt b/app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt
similarity index 59%
rename from app/src/main/java/space/taran/arkrate/MainActivity.kt
rename to app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt
index 857b57d22..1935cb17d 100644
--- a/app/src/main/java/space/taran/arkrate/MainActivity.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt
@@ -1,20 +1,19 @@
-package space.taran.arkrate
+package space.taran.arkrate.presentation
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
-import space.taran.arkrate.ui.Create
+import space.taran.arkrate.presentation.theme.ARKRateTheme
class MainActivity : ComponentActivity() {
- @RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
- Create(
- filePath = getExternalFilesDir("database")?.absolutePath.toString() + "/Currencies.json"
- )
+ ARKRateTheme {
+ MainScreen()
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt
new file mode 100644
index 000000000..ef48cf3a3
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt
@@ -0,0 +1,48 @@
+package space.taran.arkrate.presentation
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.runtime.Composable
+import com.google.accompanist.navigation.animation.AnimatedNavHost
+import com.google.accompanist.navigation.animation.composable
+import com.google.accompanist.navigation.animation.rememberAnimatedNavController
+import space.taran.arkrate.presentation.addcurrency.AddCurrencyScreen
+import space.taran.arkrate.presentation.assets.AssetsScreen
+import space.taran.arkrate.presentation.summary.SummaryScreen
+
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun MainScreen() {
+ val navController = rememberAnimatedNavController()
+ AnimatedNavHost(navController, startDestination = Screen.Assets.name) {
+ composable(Screen.Assets.name) {
+ AssetsScreen(navController)
+ }
+ composable(
+ Screen.AddCurrency.name,
+ enterTransition = {
+ fadeIn()
+ },
+ exitTransition = {
+ fadeOut()
+ }
+ ) {
+ AddCurrencyScreen(navController)
+ }
+ composable(Screen.Summary.name,
+ enterTransition = {
+ fadeIn()
+ },
+ exitTransition = {
+ fadeOut()
+ }
+ ) {
+ SummaryScreen()
+ }
+ }
+}
+
+enum class Screen {
+ Assets, AddCurrency, Summary
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt
new file mode 100644
index 000000000..abbfbf5ca
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt
@@ -0,0 +1,78 @@
+package space.taran.arkrate.presentation.addcurrency
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Divider
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import space.taran.arkrate.di.DIManager
+
+@Composable
+fun AddCurrencyScreen(navController: NavController) {
+ val viewModel: AddCurrencyViewModel =
+ viewModel(factory = DIManager.component.addCurrencyVMFactory())
+ var filter by remember { mutableStateOf("") }
+ val filteredCurrencyNameList = viewModel.currencyNameList?.filter { (code, _) ->
+ code.startsWith(filter.uppercase())
+ } ?: emptyList()
+
+ Column(Modifier.fillMaxSize()) {
+ Row {
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, top = 16.dp),
+ value = filter,
+ onValueChange = { filter = it },
+ label = {
+ Text("Search")
+ }
+ )
+ }
+ LazyColumn {
+ items(filteredCurrencyNameList.sortedBy { it.code }) { currencyName ->
+ CurrencyItem(
+ code = currencyName.code,
+ currency = currencyName.name,
+ onAdd = {
+ viewModel.addCurrency(currencyName.code)
+ navController.popBackStack()
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun CurrencyItem(code: String, currency: String, onAdd: () -> Unit) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .clickable(onClick = { onAdd() })
+ .padding(horizontal = 20.dp)
+ ) {
+ Spacer(modifier = Modifier.height(2.dp))
+ Box(Modifier.fillMaxWidth()) {
+ Text(
+ modifier = Modifier.align(Alignment.CenterStart),
+ text = code,
+ fontSize = 20.sp
+ )
+ Text(
+ modifier = Modifier.align(Alignment.CenterEnd),
+ text = currency,
+ )
+ }
+ Divider()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyViewModel.kt b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyViewModel.kt
new file mode 100644
index 000000000..42eea8864
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyViewModel.kt
@@ -0,0 +1,41 @@
+package space.taran.arkrate.presentation.addcurrency
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import space.taran.arkrate.data.CurrencyName
+import space.taran.arkrate.data.GeneralCurrencyRepo
+import space.taran.arkrate.data.assets.AssetsRepo
+import javax.inject.Inject
+import javax.inject.Singleton
+
+class AddCurrencyViewModel(
+ private val assetsRepo: AssetsRepo,
+ private val currencyRepo: GeneralCurrencyRepo
+): ViewModel() {
+ var currencyNameList by mutableStateOf?>(null)
+
+ init {
+ viewModelScope.launch {
+ currencyNameList = currencyRepo.getCurrencyName()
+ }
+ }
+
+ fun addCurrency(code: String) = viewModelScope.launch {
+ assetsRepo.setCurrencyAmount(code, 0.0)
+ }
+}
+
+@Singleton
+class AddCurrencyViewModelFactory @Inject constructor(
+ private val assetsRepo: AssetsRepo,
+ private val currencyRepo: GeneralCurrencyRepo
+): ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return AddCurrencyViewModel(assetsRepo, currencyRepo) as T
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt
new file mode 100644
index 000000000..626008eb6
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt
@@ -0,0 +1,120 @@
+package space.taran.arkrate.presentation.assets
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.List
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import space.taran.arkrate.data.CurrencyAmount
+import space.taran.arkrate.di.DIManager
+import space.taran.arkrate.presentation.Screen
+import space.taran.arkrate.utils.removeFractionalPartIfEmpty
+
+@Composable
+fun AssetsScreen(navController: NavController) {
+ val viewModel: AssetsViewModel =
+ viewModel(factory = DIManager.component.assetsVMFactory())
+
+ Box(Modifier.fillMaxSize()) {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(
+ viewModel.currencyAmountList,
+ key = { it.code }
+ ) { currencyAmount ->
+ CurrencyEditItem(
+ modifier = Modifier,
+ currencyAmount,
+ viewModel
+ )
+ }
+ }
+ FloatingActionButton(
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(10.dp),
+ onClick = { navController.navigate(Screen.AddCurrency.name) },
+ ) {
+ Icon(Icons.Filled.Add, contentDescription = "Add")
+ }
+ FloatingActionButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(10.dp),
+ onClick = { navController.navigate(Screen.Summary.name) },
+ ) {
+ Icon(Icons.Filled.List, contentDescription = "Summary")
+ }
+ }
+}
+
+@Composable
+private fun CurrencyEditItem(
+ modifier: Modifier,
+ currencyAmount: CurrencyAmount,
+ viewModel: AssetsViewModel,
+) {
+ val code = currencyAmount.code
+ var amountInput by remember {
+ mutableStateOf(
+ if (currencyAmount.amount == 0.0) ""
+ else currencyAmount.amount.removeFractionalPartIfEmpty()
+ )
+ }
+ val clearIcon = @Composable {
+ IconButton(onClick = {
+ amountInput =
+ viewModel.onAmountChanged(code, amountInput, "")
+ }) {
+ Icon(
+ Icons.Default.Clear,
+ contentDescription = ""
+ )
+ }
+ }
+ Row(modifier.padding(horizontal = 20.dp, vertical = 4.dp)) {
+ OutlinedTextField(
+ modifier = Modifier.weight(1f),
+ value = amountInput,
+ onValueChange = { newInput ->
+ amountInput = viewModel.onAmountChanged(
+ code,
+ amountInput,
+ newInput
+ )
+ },
+ trailingIcon = clearIcon,
+ label = { Text(code) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ maxLines = 1,
+ )
+ IconButton(
+ modifier = Modifier.padding(8.dp),
+ onClick = { viewModel.onCurrencyRemoved(code) }
+ ) {
+ Icon(Icons.Filled.Delete, "Delete")
+ }
+ }
+}
diff --git a/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsViewModel.kt b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsViewModel.kt
new file mode 100644
index 000000000..f0864e457
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsViewModel.kt
@@ -0,0 +1,61 @@
+package space.taran.arkrate.presentation.assets
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import space.taran.arkrate.data.CurrencyAmount
+import space.taran.arkrate.data.assets.AssetsRepo
+import javax.inject.Inject
+import javax.inject.Singleton
+
+class AssetsViewModel(
+ private val assetsRepo: AssetsRepo
+): ViewModel() {
+ var currencyAmountList = mutableStateListOf()
+
+ init {
+ viewModelScope.launch {
+ assetsRepo.allCurrencyAmountFlow().collect {
+ currencyAmountList.clear()
+ currencyAmountList.addAll(it)
+ }
+ }
+ }
+
+ fun onAmountChanged(code: String, oldInput: String, newInput: String): String {
+ val containsDigitsAndDot = Regex("[0-9]*\\.?[0-9]*")
+ if (!containsDigitsAndDot.matches(newInput))
+ return oldInput
+
+ val containsDigit = Regex(".*[0-9].*")
+ if (!containsDigit.matches(newInput)) {
+ viewModelScope.launch {
+ assetsRepo.setCurrencyAmount(code, 0.0)
+ }
+ return newInput
+ }
+
+ viewModelScope.launch {
+ assetsRepo.setCurrencyAmount(code, newInput.toDouble())
+ }
+
+ val leadingZeros = "^0+(?=\\d)".toRegex()
+
+ return newInput.replace(leadingZeros,"")
+ }
+
+ fun onCurrencyRemoved(code: String) = viewModelScope.launch {
+ assetsRepo.removeCurrency(code)
+ }
+}
+
+@Singleton
+class AssetsViewModelFactory @Inject constructor(
+ private val assetsRepo: AssetsRepo
+): ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return AssetsViewModel(assetsRepo) as T
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt
new file mode 100644
index 000000000..efc0ee98f
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt
@@ -0,0 +1,122 @@
+package space.taran.arkrate.presentation.summary
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Card
+import androidx.compose.material.Divider
+import androidx.compose.material.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import space.taran.arkrate.di.DIManager
+import java.math.RoundingMode
+import java.text.DecimalFormat
+
+private val format = DecimalFormat("0.######").apply {
+ roundingMode = RoundingMode.HALF_DOWN
+}
+
+@Composable
+fun SummaryScreen() {
+ val viewModel: SummaryViewModel =
+ viewModel(factory = DIManager.component.summaryViewModelFactory())
+ Box(Modifier.fillMaxSize()) {
+ LazyColumn(modifier = Modifier.align(Alignment.Center)) {
+ item {
+ TotalCard(viewModel)
+ }
+ item {
+ ExchangeCard(viewModel)
+ }
+ }
+ }
+}
+
+@Composable
+private fun TotalCard(viewModel: SummaryViewModel) {
+ val total by viewModel.total.collectAsState()
+ total ?: return
+ var visible by remember { mutableStateOf(true) }
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 15.dp)
+ .clickable {
+ visible = !visible
+ },
+ elevation = 8.dp
+ ) {
+ Column(modifier = Modifier.padding(8.dp)) {
+ Text("TOTAL", fontSize = 24.sp)
+ Divider()
+ AnimatedVisibility(visible) {
+ Column {
+ total!!.forEach {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(32.dp)
+ ) {
+ Text(
+ it.key,
+ modifier = Modifier.align(Alignment.CenterStart)
+ )
+ Text(
+ format.format(it.value),
+ modifier = Modifier.align(Alignment.CenterEnd)
+ )
+ Divider()
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ExchangeCard(viewModel: SummaryViewModel) {
+ val exchange by viewModel.exchange.collectAsState()
+ exchange ?: return
+ var visible by remember { mutableStateOf(true) }
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 15.dp)
+ .clickable {
+ visible = !visible
+ },
+ elevation = 8.dp
+ ) {
+ Column(modifier = Modifier.padding(8.dp)) {
+ Text("EXCHANGE", fontSize = 24.sp)
+ Divider()
+ AnimatedVisibility(visible) {
+ Column {
+ exchange!!.forEach {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(32.dp)
+ ) {
+ Text(
+ it.key,
+ modifier = Modifier.align(Alignment.CenterStart)
+ )
+ Text(
+ format.format(it.value),
+ modifier = Modifier.align(Alignment.CenterEnd)
+ )
+ Divider()
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt
new file mode 100644
index 000000000..4b2aa0211
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt
@@ -0,0 +1,73 @@
+package space.taran.arkrate.presentation.summary
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import space.taran.arkrate.data.GeneralCurrencyRepo
+import space.taran.arkrate.data.assets.AssetsRepo
+import javax.inject.Inject
+import javax.inject.Singleton
+
+class SummaryViewModel(
+ private val assetsRepo: AssetsRepo,
+ private val currencyRepo: GeneralCurrencyRepo
+) : ViewModel() {
+ var total = MutableStateFlow