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?>(null) + var exchange = MutableStateFlow?>(null) + + init { + viewModelScope.launch { + calculateTotal() + calculateExchange() + } + } + + private suspend fun calculateTotal() { + val count = assetsRepo.allCurrencyAmount().map { + it.code to it.amount + }.toMap() + val rates = currencyRepo.getCurrencyRate().associate { it.code to it.rate } + val result = mutableMapOf() + var USD = 0.0 + + count.forEach { + USD += it.value * rates[it.key]!! + } + + count.forEach { + result[it.key] = USD / rates[it.key]!! + } + + total.emit(result) + } + + private suspend fun calculateExchange() { + val count = assetsRepo.allCurrencyAmount().map { + it.code to it.amount + }.toMap() + val rates = currencyRepo.getCurrencyRate().associate { it.code to it.rate } + val result = mutableMapOf() + + count.forEach { i -> + count.forEach { j -> + if (j.key != i.key) { + result["${i.key}/${j.key}"] = rates[i.key]!! / rates[j.key]!! + } + } + } + + exchange.emit(result) + } +} + +@Singleton +class SummaryViewModelFactory @Inject constructor( + private val assetsRepo: AssetsRepo, + private val currencyRepo: GeneralCurrencyRepo +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return SummaryViewModel(assetsRepo, currencyRepo) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/theme/Color.kt b/app/src/main/java/space/taran/arkrate/presentation/theme/Color.kt similarity index 79% rename from app/src/main/java/space/taran/arkrate/ui/theme/Color.kt rename to app/src/main/java/space/taran/arkrate/presentation/theme/Color.kt index 66982b5cf..c54c483ea 100644 --- a/app/src/main/java/space/taran/arkrate/ui/theme/Color.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/theme/Color.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate.ui.theme +package space.taran.arkrate.presentation.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/space/taran/arkrate/ui/theme/Shape.kt b/app/src/main/java/space/taran/arkrate/presentation/theme/Shape.kt similarity index 85% rename from app/src/main/java/space/taran/arkrate/ui/theme/Shape.kt rename to app/src/main/java/space/taran/arkrate/presentation/theme/Shape.kt index 49cc592e1..8c89adcbe 100644 --- a/app/src/main/java/space/taran/arkrate/ui/theme/Shape.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/theme/Shape.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate.ui.theme +package space.taran.arkrate.presentation.theme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Shapes diff --git a/app/src/main/java/space/taran/arkrate/ui/theme/Theme.kt b/app/src/main/java/space/taran/arkrate/presentation/theme/Theme.kt similarity index 87% rename from app/src/main/java/space/taran/arkrate/ui/theme/Theme.kt rename to app/src/main/java/space/taran/arkrate/presentation/theme/Theme.kt index 74b1f3b1f..cbbf7799e 100644 --- a/app/src/main/java/space/taran/arkrate/ui/theme/Theme.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/theme/Theme.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate.ui.theme +package space.taran.arkrate.presentation.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.MaterialTheme @@ -28,7 +28,7 @@ private val LightColorPalette = lightColors( ) @Composable -fun ExchangeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { +fun ARKRateTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colors = if (darkTheme) { DarkColorPalette } else { diff --git a/app/src/main/java/space/taran/arkrate/ui/theme/Type.kt b/app/src/main/java/space/taran/arkrate/presentation/theme/Type.kt similarity index 94% rename from app/src/main/java/space/taran/arkrate/ui/theme/Type.kt rename to app/src/main/java/space/taran/arkrate/presentation/theme/Type.kt index e4b1c7cb7..453cd2c07 100644 --- a/app/src/main/java/space/taran/arkrate/ui/theme/Type.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/theme/Type.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate.ui.theme +package space.taran.arkrate.presentation.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/java/space/taran/arkrate/storage/Input.kt b/app/src/main/java/space/taran/arkrate/storage/Input.kt deleted file mode 100644 index d6f684746..000000000 --- a/app/src/main/java/space/taran/arkrate/storage/Input.kt +++ /dev/null @@ -1,74 +0,0 @@ -package space.taran.arkrate.storage - -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import java.io.File - -@JsonClass(generateAdapter = true) -data class total( - val total: MutableList -) - -@JsonClass(generateAdapter = true) -data class Exchange( - var name: String, - var number: Double -) - - -class AppDatabase(val filePath: String) { - - fun getAllExchange(): List { - if (!File(filePath).canWrite()) { - File(filePath).writeText("""{"total":[]}""") - } - val origin = File(filePath).readText() - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(total::class.java) - val json = jsonAdapter.fromJson(origin) - return json?.total ?: listOf() - } - - fun setExchange(name: String, value: Double) { - - if (!File(filePath).canWrite()) { - File(filePath).writeText("""{"total":[]}""") - } - val origin = File(filePath).readText() - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(total::class.java) - val json = jsonAdapter.fromJson(origin) ?: total(mutableListOf()) - for (i in (0 until if (json.total.size == 0) { - 1 - } else { - json.total.size - })) { - if (json.total.size > 0 && json.total[i].name == name) { - json.total[i].number = value - break - } - if (json.total.size == 0 || i == json.total.size - 1) { - json.total.add(Exchange(name, value)) - } - } - File(filePath).writeText(jsonAdapter.toJson(json)) - } - - fun remove(name: String) { - if (!File(filePath).canWrite()) { - File(filePath).writeText("""{"total":[]}""") - } - val origin = File(filePath).readText() - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(total::class.java) - val json = jsonAdapter.fromJson(origin) ?: total(mutableListOf()) - - for (i in (0 until json.total.size)) { - if (json.total[i].name == name) { - json.total.removeAt(i) - break - } - } - File(filePath).writeText(jsonAdapter.toJson(json)) - } -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/AddExchange.kt b/app/src/main/java/space/taran/arkrate/ui/AddExchange.kt deleted file mode 100644 index 7f2f4bb8a..000000000 --- a/app/src/main/java/space/taran/arkrate/ui/AddExchange.kt +++ /dev/null @@ -1,78 +0,0 @@ -package space.taran.arkrate.ui - -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.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.unit.dp -import space.taran.arkrate.network.currencies -import space.taran.arkrate.storage.AppDatabase - -class AddExchange( - val appDatabase: AppDatabase, - val count: SnapshotStateMap, - val visible: SnapshotStateMap, -) { - @Composable - fun AddExchangeView( - modifier: Modifier, - searchResult: SnapshotStateMap, - filePath: String, - setNowActivity: () -> Unit, - ) { - var searchContent by remember { mutableStateOf("") } - val focusManager = LocalFocusManager.current - return Column(modifier = modifier) { - Row { - OutlinedTextField( - value = searchContent, - onValueChange = { it1 -> - searchContent = it1 - for (i in searchResult.keys.size - 1 downTo 0) { - searchResult.remove(searchResult.keys.toList()[i]) - } - val from = currencies.get(filePath = filePath).filter { - return@filter it.key.indexOf(it1.uppercase()) != -1 - } - searchResult.putAll(from) - }, - modifier = Modifier.fillMaxWidth(), - label = { - Text("Search") - } - ) - } - LazyColumn { - items(searchResult.keys.toList().sorted()) { - Box(Modifier.fillMaxWidth().height(32.dp).clickable(onClick = { - appDatabase.setExchange(it, -1.0) - count[it] = "-1.0" - visible[it] = true - focusManager.clearFocus() - setNowActivity() - })) { - Text( - it, - modifier = Modifier.align(Alignment.CenterStart), - ) - currencies.currencies[it]?.let { it1 -> - Text( - it1, - modifier = Modifier.align(Alignment.CenterEnd) - ) - } - } - Divider() - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/InputActivity.kt b/app/src/main/java/space/taran/arkrate/ui/InputActivity.kt deleted file mode 100644 index 301532441..000000000 --- a/app/src/main/java/space/taran/arkrate/ui/InputActivity.kt +++ /dev/null @@ -1,111 +0,0 @@ -package space.taran.arkrate.ui - -import android.util.Log -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.text.KeyboardOptions -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.Clear -import androidx.compose.material.icons.filled.Delete -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import space.taran.arkrate.storage.AppDatabase - -class InputActivity( - val appDatabase: AppDatabase, - val count: SnapshotStateMap, - val visible: SnapshotStateMap -) { - @Composable - fun InputView(modifier: Modifier) { - return LazyColumn(modifier = modifier) { - items(count.size) { i -> - AnimatedVisibility( - visible[count.keys.toList()[i]]!!, - exit = slideOutHorizontally() - ) { - Row(modifier.fillMaxWidth()) { - var textValue by remember { mutableStateOf(if ((count[count.keys.toList()[i]]?: "-1.0").toDouble() == -1.0 - ) "" else count[count.keys.toList()[i]].toString()) } - OutlinedTextField( - value = textValue, - onValueChange = { - textValue=it - var oneDot = true - val result = it.filter { - if (it == ".".toCharArray()[0] && oneDot) { - oneDot = false - Log.d("inputview", "InputView: $it") - return@filter true - } - return@filter it.isDigit() - } - appDatabase.setExchange( - count.keys.toList()[i], - if (result.isEmpty()) { - -1.0 - } else { - result.toDouble() - } - ) - count[count.keys.toList()[i]] = - (result.ifEmpty { (-1.0).toString() }) - }, - trailingIcon = { - Icon(Icons.Default.Clear, - contentDescription = "clear text", - modifier = Modifier - .clickable { - appDatabase.setExchange(count.keys.toList()[i], -1.0) - count[count.keys.toList()[i]] = "-1.0" - visible[count.keys.toList()[i]] = true - textValue="" - } - ) - }, - label = { Text(count.keys.toList()[i]) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.height(60.dp).fillMaxWidth(0.9f), - maxLines = 1 - ) - IconButton(onClick = { - appDatabase.remove(count.keys.toList()[i]) - visible[count.keys.toList()[i]] = false - }, modifier = Modifier.padding(8.dp)) { - Icon(Icons.Filled.Delete, "Delete") - } - } - } - } - - //if don't have any exchange show this text - if (count.size == 0) { - item { - Box(modifier = Modifier.fillMaxSize()) { - Text( - modifier = Modifier.align(Alignment.Center), - text = "Click the Add button at the top right to add a new exchange rate." - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/MainActivity.kt b/app/src/main/java/space/taran/arkrate/ui/MainActivity.kt deleted file mode 100644 index 785723e19..000000000 --- a/app/src/main/java/space/taran/arkrate/ui/MainActivity.kt +++ /dev/null @@ -1,220 +0,0 @@ -package space.taran.arkrate.ui - -import android.app.Activity -import android.os.Build -import androidx.activity.compose.BackHandler -import androidx.annotation.RequiresApi -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.ExtendedFloatingActionButton -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowForward -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import coil.ImageLoader -import coil.compose.rememberAsyncImagePainter -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder -import kotlin.concurrent.thread -import kotlinx.coroutines.launch -import space.taran.arkrate.R -import space.taran.arkrate.network.currencies -import space.taran.arkrate.storage.AppDatabase -import space.taran.arkrate.ui.theme.ExchangeTheme - -@RequiresApi(Build.VERSION_CODES.N) -@Composable -fun Create(filePath: String) { - val scope = rememberCoroutineScope() - val appDatabase = AppDatabase(filePath) - val scaffoldState = rememberScaffoldState() - val count = remember { mutableStateMapOf() } - val visibilities = remember { mutableStateMapOf() } - val viewVisibility = remember { - mutableStateMapOf( - Views.Input to true, - Views.Output to false, - Views.Add to false - ) - } - appDatabase.getAllExchange().forEach { - count[it.name] = it.number.toString() - visibilities[it.name] = true - } - var nowActivity by remember { - mutableStateOf(Views.Input) - } - val activity = (LocalContext.current as? Activity) - - BackHandler(true, onBack = { - if (nowActivity != Views.Input) { - viewVisibility[Views.Input] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Input - } else - activity?.finish() - }) - return ExchangeTheme { - Scaffold( - scaffoldState = scaffoldState, - topBar = { - TopAppBar( - title = { Text(text = Views.Input.name) }, - navigationIcon = { - IconButton(onClick = { scope.launch { scaffoldState.drawerState.open() } }) { - Icon(Icons.Filled.Menu, contentDescription = null) - } - }, - actions = { - IconButton(onClick = { - viewVisibility[nowActivity] = false - viewVisibility[Views.Add] = true - nowActivity = Views.Add - - }) { - Icon(Icons.Filled.Add, contentDescription = "add transformation") - } - } - ) - }, - content = { contentPadding -> - Box(modifier = Modifier.padding(contentPadding)) { - AnimatedVisibility(viewVisibility[Views.Input]!!) { - InputActivity( - appDatabase, count, visibilities - ).InputView( - modifier = Modifier - .fillMaxSize() - .padding(start = 16.dp, top = 16.dp, end = 16.dp), - ) - } - AnimatedVisibility(viewVisibility[Views.Output]!!) { - OutputActivity().activity( - modifier = Modifier - .fillMaxSize() - .padding(start = 16.dp, top = 16.dp, end = 16.dp), count, filePath - ) - } - AnimatedVisibility(viewVisibility[Views.Add]!!) { - Box(Modifier.fillMaxSize()) { - var loading by remember { mutableStateOf(true) } - val a = remember> { mutableStateMapOf() } - thread { - a.putAll( - currencies.get(filePath) - ) - loading = false - } - AnimatedVisibility( - loading, - modifier = Modifier.align(Alignment.Center), - exit = fadeOut() - ) { - Column { - val imgLoader = ImageLoader.Builder(LocalContext.current) - .components { - if (Build.VERSION.SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() - val mPainter = rememberAsyncImagePainter( - R.drawable.loading, - imgLoader - ) - Image( - painter = mPainter, - "Loading...", - modifier = Modifier.size(64.dp) - ) - Text("Loading...") - } - } - AnimatedVisibility(!loading) { - AddExchange(appDatabase, count, visibilities).AddExchangeView( - Modifier - .fillMaxSize() - .padding(16.dp, 16.dp, 16.dp), - a, - filePath - ) { - viewVisibility[Views.Input] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Input - } - } - } - } - } - }, - drawerContent = { - Column(modifier = Modifier.fillMaxSize()) { - Text(text = "not yet") - } - }, - floatingActionButton = { - ExtendedFloatingActionButton(text = { - when (nowActivity) { - Views.Input -> Icon(Icons.Filled.ArrowForward, "go to output") - Views.Output -> Icon(Icons.Filled.ArrowBack, "back to Input") - Views.Add -> Icon(Icons.Filled.ArrowBack, "back to Input") - } - }, onClick = { - visibilities.forEach { (s, b) -> - if (!b) { - count.remove(s) - } - } - when (nowActivity) { - Views.Add -> { - viewVisibility[Views.Input] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Input - } - Views.Input -> { - viewVisibility[Views.Output] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Output - } - Views.Output -> { - viewVisibility[Views.Input] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Input - } - } - }) - } - ) - } -} - -enum class Views { - Input, Output, Add -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/OutputActivity.kt b/app/src/main/java/space/taran/arkrate/ui/OutputActivity.kt deleted file mode 100644 index a1adc0539..000000000 --- a/app/src/main/java/space/taran/arkrate/ui/OutputActivity.kt +++ /dev/null @@ -1,224 +0,0 @@ -package space.taran.arkrate.ui - -import android.os.Build.VERSION.SDK_INT -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -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.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.ImageLoader -import coil.compose.rememberAsyncImagePainter -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder -import space.taran.arkrate.network.Crypto -import space.taran.arkrate.network.getAllRatesWithCache -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.DecimalFormat -import kotlin.concurrent.thread - -class OutputActivity { - @Composable - fun activity( - modifier: Modifier, - count: SnapshotStateMap, - filePath: String, - ) { - var loading by remember { mutableStateOf(true) } - var rates: MutableMap - val total: SnapshotStateMap = remember { mutableStateMapOf() } - val exchange: SnapshotStateMap = remember { mutableStateMapOf() } - thread { - rates = getAllRatesWithCache(filePath).toMutableMap() - Crypto().getCryptoPriceWithCache(filePath).map { - if (it.key.takeLast(4) == "USDT") { - rates[it.key.dropLast(4)] = UtilsBigDecimal.div(1.0, it.value) - } - } - Thread.sleep(500) - calculateTotal(count, rates).forEach { - total[it.key] = it.value - } - calculateExchange(count, rates).forEach { - exchange[it.key] = it.value - } - loading = false - } - return Box(modifier = modifier) { - AnimatedVisibility( - loading, - modifier = Modifier.align(Alignment.Center), - exit = fadeOut() - ) { - Column { - val imgLoader = ImageLoader.Builder(LocalContext.current) - .components { - if (SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() - val mPainter = rememberAsyncImagePainter( - space.taran.arkrate.R.drawable.loading, - imgLoader - ) - Image(painter = mPainter, "Loading...", modifier = Modifier.size(64.dp)) - Text("Loading...") - } - } - AnimatedVisibility(!loading) { - LazyColumn { - item { - Card( - modifier = Modifier - .fillMaxWidth(), elevation = 8.dp - ) { - Column(modifier = Modifier.padding(8.dp)) { - Text("TOTAL", fontSize = 24.sp) - Divider() - total.forEach { - Box(modifier = Modifier.fillMaxWidth().height(32.dp)) { - val format = DecimalFormat("0.###") - format.roundingMode = RoundingMode.HALF_DOWN - Text( - it.key, - modifier = Modifier.align(Alignment.CenterStart) - ) - Text( - format.format(it.value), - modifier = Modifier.align(Alignment.CenterEnd) - ) - Divider() - } - } - } - } - } - item { - Spacer(Modifier.fillMaxWidth().height(16.dp)) - } - item { - Card( - modifier = Modifier.fillMaxWidth(), - elevation = 8.dp - ) { - Column(modifier = Modifier.padding(8.dp)) { - var visible by remember { mutableStateOf(true) } - Box(modifier = Modifier.clickable(onClick = { - visible = !visible - }).fillMaxWidth()) { - Text("EXCHANGE", fontSize = 24.sp) - } - Divider() - AnimatedVisibility(visible) { - Column { - exchange.forEach { - Box(modifier = Modifier.fillMaxWidth().height(32.dp)) { - val format = DecimalFormat("0.###") - format.roundingMode = RoundingMode.HALF_DOWN - Text( - it.key, - modifier = Modifier.align(Alignment.CenterStart) - ) - Text( - format.format(it.value), - modifier = Modifier.align(Alignment.CenterEnd) - ) - Divider() - } - } - } - } - } - } - } - item { - Spacer(Modifier.fillMaxWidth().height(64.dp)) - } - } - } - } - } - - private fun calculateTotal( - count: SnapshotStateMap, - rates: Map - ): Map { - val result = mutableMapOf() - var USD = 0.0 - - count.forEach { - USD += UtilsBigDecimal.mul( - if ((if (it.value.isEmpty()) 0.0 else it.value.toDouble()) == -1.0) 0.0 else it.value.toDouble(), - UtilsBigDecimal.div(1.0, rates[it.key]!!) - ) - } - - count.forEach { - result[it.key] = UtilsBigDecimal.mul(USD, rates[it.key]!!) - } - - return result - } - - private fun calculateExchange( - count: SnapshotStateMap, - rates: Map - ): Map { - - val result = mutableMapOf() - var USD = 0.0 - - count.forEach { - USD += UtilsBigDecimal.mul( - if (it.value.isEmpty()) 0.0 else it.value.toDouble(), - UtilsBigDecimal.div(1.0, rates[it.key]!!) - ) - } - - count.forEach { i -> - val iToUsd = UtilsBigDecimal.div(1.0, rates[i.key]!!) - count.forEach { j -> - if (j.key != i.key) { - result["${i.key}/${j.key}"] = - UtilsBigDecimal.mul(iToUsd, rates[j.key]!!) - } - } - } - - return result - } -} - -private object UtilsBigDecimal { - - // 需要精确至小数点后几位 - const val DECIMAL_POINT_NUMBER: Int = 8 - - // 加法运算 - @JvmStatic - fun mul(d1: Double, d2: Double): Double = - BigDecimal(d1).multiply(BigDecimal(d2)) - .setScale(DECIMAL_POINT_NUMBER, BigDecimal.ROUND_DOWN) - .toDouble() - - // 除法运算 - @JvmStatic - fun div(d1: Double, d2: Double): Double = - BigDecimal(d1).divide(BigDecimal(d2), DECIMAL_POINT_NUMBER, BigDecimal.ROUND_DOWN) - .toDouble() - -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/utils/CollectionsUtils.kt b/app/src/main/java/space/taran/arkrate/utils/CollectionsUtils.kt new file mode 100644 index 000000000..14f333c0b --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/utils/CollectionsUtils.kt @@ -0,0 +1,9 @@ +package space.taran.arkrate.utils + +fun List.replace(targetItem: T, newItem: T) = map { + if (it == targetItem) { + newItem + } else { + it + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/utils/CoroutineUtils.kt b/app/src/main/java/space/taran/arkrate/utils/CoroutineUtils.kt new file mode 100644 index 000000000..80d506513 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/utils/CoroutineUtils.kt @@ -0,0 +1,17 @@ +package space.taran.arkrate.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext + +suspend fun withContextAndLock( + context: CoroutineContext, + mutex: Mutex, + block: suspend CoroutineScope.() -> T +): T = withContext(context) { + mutex.withLock { + block() + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt b/app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt new file mode 100644 index 000000000..a82da5391 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt @@ -0,0 +1,10 @@ +package space.taran.arkrate.utils + +fun Double.removeFractionalPartIfEmpty(): String { + val integerPart = this.toInt() + val fractionalPart = integerPart - this + return if (fractionalPart == 0.0) + integerPart.toString() + else + this.toString() +} \ No newline at end of file diff --git a/app/src/main/res/drawable/loading.gif b/app/src/main/res/drawable/loading.gif deleted file mode 100644 index cbaf6414a..000000000 Binary files a/app/src/main/res/drawable/loading.gif and /dev/null differ diff --git a/build.gradle b/build.gradle index 8c50d0525..54694c7f1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - compose_version = '1.3.0-beta01' + compose_version = '1.3.0' } ext.kotlin_version="1.7.10" }// Top-level build file where you can add configuration options common to all sub-projects/modules.