├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── ic_logo-web.png
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_logo.png
│ │ │ │ └── ic_logo_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_logo.png
│ │ │ │ └── ic_logo_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_logo.png
│ │ │ │ └── ic_logo_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_logo.png
│ │ │ │ └── ic_logo_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_logo.png
│ │ │ │ └── ic_logo_round.png
│ │ │ ├── values
│ │ │ │ ├── boolean.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── values-v23
│ │ │ │ └── boolean.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_logo.xml
│ │ │ │ └── ic_logo_round.xml
│ │ │ ├── drawable
│ │ │ │ ├── ripple.xml
│ │ │ │ ├── ripple_dark.xml
│ │ │ │ ├── ic_swap_button.xml
│ │ │ │ ├── ic_logo_background.xml
│ │ │ │ └── ic_logo_foreground.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_picker.xml
│ │ │ │ ├── row_picker.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ └── numpad.xml
│ │ │ └── layout-land
│ │ │ │ └── activity_main.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── worker8
│ │ │ │ └── simplecurrency
│ │ │ │ ├── di
│ │ │ │ ├── scope
│ │ │ │ │ ├── PerActivityScope.kt
│ │ │ │ │ └── ScopeConstant.kt
│ │ │ │ ├── module
│ │ │ │ │ ├── RepoModule.kt
│ │ │ │ │ ├── ActivityModule.kt
│ │ │ │ │ └── AppModule.kt
│ │ │ │ └── AppComponent.kt
│ │ │ │ ├── db
│ │ │ │ ├── entity
│ │ │ │ │ ├── RoomUpdatedTimeStamp.kt
│ │ │ │ │ └── RoomConversionRate.kt
│ │ │ │ ├── dao
│ │ │ │ │ ├── RoomUpdatedTimeStampDao.kt
│ │ │ │ │ ├── BaseDao.kt
│ │ │ │ │ └── RoomConversionRateDao.kt
│ │ │ │ └── SimpleCurrencyDatabase.kt
│ │ │ │ ├── common
│ │ │ │ ├── extension
│ │ │ │ │ ├── RxExtension.kt
│ │ │ │ │ └── DoubleExtension.kt
│ │ │ │ ├── util
│ │ │ │ │ └── NumberFormatterUtil.kt
│ │ │ │ └── sharedPreference
│ │ │ │ │ ├── MainPreference.kt
│ │ │ │ │ └── PreferenceUtil.kt
│ │ │ │ ├── ui
│ │ │ │ ├── picker
│ │ │ │ │ ├── PickerContract.kt
│ │ │ │ │ ├── PickerViewHolder.kt
│ │ │ │ │ ├── PickerAdapter.kt
│ │ │ │ │ ├── PickerRepo.kt
│ │ │ │ │ ├── PickerActivity.kt
│ │ │ │ │ └── PickerViewModel.kt
│ │ │ │ └── main
│ │ │ │ │ ├── MainRepoInterface.kt
│ │ │ │ │ ├── event
│ │ │ │ │ ├── BackSpaceInputEvent.kt
│ │ │ │ │ ├── NewNumberInputEvent.kt
│ │ │ │ │ └── CalculateConversionRateEvent.kt
│ │ │ │ │ ├── MainContract.kt
│ │ │ │ │ ├── MainRepo.kt
│ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ └── MainViewModel.kt
│ │ │ │ ├── SimpleCurrencyApplication.kt
│ │ │ │ └── worker
│ │ │ │ └── UpdateCurrencyWorker.kt
│ │ └── AndroidManifest.xml
│ ├── release
│ │ └── java
│ │ │ └── com
│ │ │ └── worker8
│ │ │ └── simplecurrency
│ │ │ ├── DebugSetting.kt
│ │ │ └── OkHttpClientProvider.kt
│ ├── debug
│ │ └── java
│ │ │ └── com
│ │ │ └── worker8
│ │ │ └── simplecurrency
│ │ │ ├── OkHttpClientProvider.kt
│ │ │ └── DebugSetting.kt
│ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── worker8
│ │ │ └── simplecurrency
│ │ │ └── RoomConversionRateDaoTest.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── worker8
│ │ └── simplecurrency
│ │ └── MainViewModelTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── fixerio
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── worker8
│ │ │ │ └── fixerio
│ │ │ │ ├── model
│ │ │ │ ├── Rates.kt
│ │ │ │ ├── Quotes.kt
│ │ │ │ ├── ConversionRate.kt
│ │ │ │ └── Currency.kt
│ │ │ │ ├── response
│ │ │ │ └── EuroCurrencyResponse.kt
│ │ │ │ ├── network
│ │ │ │ ├── FixerIOMoshi.kt
│ │ │ │ ├── FixerIOLiveService.kt
│ │ │ │ ├── SeedFixerIOLiveService.kt
│ │ │ │ └── FixerIORetrofit.kt
│ │ │ │ └── adapter
│ │ │ │ ├── QuotesCustomAdapter.kt
│ │ │ │ └── RatesCustomAdapter.kt
│ │ └── resources
│ │ │ └── usd_based_currencies_fixerio.json
│ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── worker8
│ │ │ └── fixerio
│ │ │ └── ExampleInstrumentedTest.java
│ └── test
│ │ └── java
│ │ └── com
│ │ └── worker8
│ │ └── fixerio
│ │ ├── FixerIOLiveServiceTest.kt
│ │ └── FixerIOMoshiTest.kt
├── build.gradle.kts
└── proguard-rules.pro
├── settings.gradle
├── logo.sketch
├── feature_image.sketch
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .editorconfig
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── privacy_policy.md
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/fixerio/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':fixerio'
2 |
--------------------------------------------------------------------------------
/logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/logo.sketch
--------------------------------------------------------------------------------
/feature_image.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/feature_image.sketch
--------------------------------------------------------------------------------
/app/src/main/ic_logo-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/ic_logo-web.png
--------------------------------------------------------------------------------
/fixerio/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FixerIO
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-hdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-mdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-xhdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-xxhdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-hdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-mdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-xhdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/fixerio/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-xxhdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worker8/SimpleCurrency/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/boolean.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v23/boolean.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
5 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/model/Rates.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.model
2 |
3 | class Rates {
4 | var conversionRates = listOf()
5 | }
6 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/model/Quotes.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.model
2 |
3 | class Quotes {
4 | var conversionRates = listOf()
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/release/java/com/worker8/simplecurrency/DebugSetting.kt:
--------------------------------------------------------------------------------
1 | import android.content.Context
2 |
3 | class DebugSetting {
4 | companion object {
5 | fun init(context: Context) {
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/release/java/com/worker8/simplecurrency/OkHttpClientProvider.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency
2 |
3 | import okhttp3.OkHttpClient
4 |
5 | fun provideOkHttpClient() =
6 | OkHttpClient.Builder()
7 | .build()
8 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/model/ConversionRate.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.model
2 |
3 | data class ConversionRate(val code: String, val rate: Double) {
4 | // fun getCodeWithoutUSD(): String {
5 | // return code.substring(3)
6 | // }
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/di/scope/PerActivityScope.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.di.scope
2 |
3 | import javax.inject.Scope
4 |
5 | @Retention(value = AnnotationRetention.RUNTIME)
6 | @MustBeDocumented
7 | @Scope
8 | annotation class PerActivityScope
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/di/scope/ScopeConstant.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.di.scope
2 |
3 | object ScopeConstant {
4 | const val MainThreadScheduler = "MainThreadScheduler"
5 | const val BackgroundThreadScheduler = "BackgroundThreadScheduler"
6 | }
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Sep 04 10:40:41 JST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_logo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_logo_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/debug/java/com/worker8/simplecurrency/OkHttpClientProvider.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency
2 |
3 | import okhttp3.OkHttpClient
4 | import com.facebook.stetho.okhttp3.StethoInterceptor
5 |
6 | fun provideOkHttpClient() =
7 | OkHttpClient.Builder()
8 | .addNetworkInterceptor(StethoInterceptor())
9 | .build()
10 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/response/EuroCurrencyResponse.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.response
2 |
3 | import com.worker8.fixerio.model.Rates
4 |
5 | data class EuroCurrencyResponse(
6 | val success: Boolean,
7 | val timestamp: Long,
8 | val base: String,
9 | val date: String,
10 | val rates: Rates
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #303F9F
4 | #1A237E
5 | #EF6C00
6 | #48FFFFFF
7 | #f5f5f5
8 | #BDBDBD
9 |
10 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/network/FixerIOMoshi.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.network
2 |
3 | import com.squareup.moshi.Moshi
4 | import com.worker8.fixerio.adapter.ConversionsFactory
5 |
6 | class FixerIOMoshi {
7 | companion object {
8 | fun build(): Moshi = Moshi
9 | .Builder()
10 | .add(ConversionsFactory())
11 | .build()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/db/entity/RoomUpdatedTimeStamp.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.db.entity
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity
7 | data class RoomUpdatedTimeStamp(
8 | @PrimaryKey val id: String,
9 | /* TimeStamp from the API*/
10 | val timeStamp: Long,
11 | val updatedAt: Long = System.currentTimeMillis() / 1000
12 | )
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 |
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
13 | [*.{gradle,java,kt,kts,scala,rs,xml}]
14 | indent_size = 4
15 |
16 | [{Makefile,*.go}]
17 | indent_style = tab
18 | indent_size = 4
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Simple Currency
3 | Search currencies by name or code
4 | Last Updated
5 | FROM
6 | TO
7 | Ops, there is an error, please restart the app…
8 |
9 |
--------------------------------------------------------------------------------
/app/src/debug/java/com/worker8/simplecurrency/DebugSetting.kt:
--------------------------------------------------------------------------------
1 | import android.content.Context
2 | import com.facebook.stetho.Stetho
3 | import com.worker8.simplecurrency.BuildConfig
4 |
5 | class DebugSetting {
6 | companion object {
7 | fun init(context: Context) {
8 | if (BuildConfig.DEBUG) {
9 | Stetho.initializeWithDefaults(context)
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/di/module/RepoModule.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.di.module
2 |
3 | import com.worker8.simplecurrency.ui.main.MainRepo
4 | import com.worker8.simplecurrency.ui.main.MainRepoInterface
5 | import dagger.Binds
6 | import dagger.Module
7 |
8 | @Module
9 | abstract class RepoModule {
10 | @Binds
11 | abstract fun provideMainRepoInterface(mainRepo: MainRepo): MainRepoInterface
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/db/dao/RoomUpdatedTimeStampDao.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.db.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Query
5 | import com.worker8.simplecurrency.db.entity.RoomUpdatedTimeStamp
6 |
7 | @Dao
8 | interface RoomUpdatedTimeStampDao : BaseDao {
9 | @Query("SELECT * FROM RoomUpdatedTimeStamp WHERE id='1'")
10 | fun findLatestTimeStamp(): List
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/common/extension/RxExtension.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.common
2 |
3 | import io.reactivex.disposables.CompositeDisposable
4 | import io.reactivex.disposables.Disposable
5 | import io.reactivex.subjects.BehaviorSubject
6 |
7 | fun Disposable.addTo(composite: CompositeDisposable) = composite.add(this)
8 |
9 | // convenience value to avoid using !! everywhere
10 | val BehaviorSubject.realValue: T
11 | get() = value!!
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/db/dao/BaseDao.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.db.dao
2 |
3 | import androidx.room.Delete
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Update
7 |
8 | interface BaseDao {
9 | @Insert(onConflict = OnConflictStrategy.REPLACE)
10 | fun insert(obj: T)
11 |
12 | @Insert
13 | fun insert(vararg obj: T)
14 |
15 | @Update
16 | fun update(obj: T)
17 |
18 | @Delete
19 | fun delete(obj: T)
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | local.properties
3 | api_keys.properties
4 |
5 | */build/
6 | secret.gradle
7 | proguard.map
8 |
9 | *~
10 | *.swp
11 | .DS_Store
12 | build
13 |
14 | # Android Studio
15 | .idea/*
16 | !.idea/codeStyles/Project.xml
17 | !.idea/codeStyles/codeStyleConfig.xml
18 | *.iml
19 | *.ipr
20 | *.iws
21 |
22 | # Keystore
23 | #signing.gradle
24 |
25 | # Crashlytics
26 | com_crashlytics_export_strings.xml
27 | crashlytics-build.properties
28 | crashlytics.properties
29 |
30 | # integration tests
31 | artifacts
32 |
33 | # Documentations
34 | design/
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/common/util/NumberFormatterUtil.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.common.util
2 |
3 | import com.worker8.simplecurrency.common.extension.toComma
4 |
5 | class NumberFormatterUtil {
6 | companion object {
7 | fun addComma(s: String): String {
8 | val dotIndex = s.indexOf('.')
9 | return if (dotIndex == -1) {
10 | s.toDouble().toComma()
11 | } else {
12 | s.substring(0, dotIndex).toDouble().toComma() + s.substring(dotIndex)
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | -
6 |
7 |
8 |
9 |
10 |
11 | -
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_dark.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | -
6 |
7 |
8 |
9 |
10 |
11 | -
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/network/FixerIOLiveService.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.network
2 |
3 | import com.worker8.fixerio.BuildConfig
4 | import com.worker8.fixerio.response.EuroCurrencyResponse
5 | import io.reactivex.Single
6 | import retrofit2.http.GET
7 | import retrofit2.http.Query
8 |
9 | interface FixerIOLiveService {
10 | @GET(BuildConfig.CURRENCY_API_URL_PATH)
11 | fun getCurrencies(
12 | @Query("access_key") accessKey: String = BuildConfig.FIXER_IO_ACCCES_KEY,
13 | @Query("base") source: String = "EUR",
14 | @Query("format") format: Int = 1
15 | ): Single
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/picker/PickerContract.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.picker
2 |
3 | import io.reactivex.Observable
4 |
5 |
6 | class PickerContract {
7 | interface Input {
8 | val isBase: Boolean
9 | val onFilterTextChanged: Observable
10 | val inputAmount: Double
11 | }
12 |
13 | interface ViewAction {
14 | fun showTerminalError()
15 | }
16 |
17 | data class ScreenState(
18 | val currencyList: LinkedHashSet,
19 | val rateDetailVisibility: Boolean,
20 | val latestUpdatedString: String
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/fixerio/build.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.Tool.kotlin}")
3 | implementation("com.squareup.retrofit2:retrofit:${Versions.App.retrofit}")
4 | implementation("com.squareup.retrofit2:converter-moshi:${Versions.App.retrofit}")
5 | implementation("com.squareup.retrofit2:adapter-rxjava2:${Versions.App.retrofit}")
6 | implementation("com.squareup.moshi:moshi-kotlin:${Versions.App.moshi}")
7 | kapt("com.squareup.moshi:moshi-kotlin-codegen:${Versions.App.moshi}")
8 |
9 | testImplementation("junit:junit:${Versions.Test.junit}")
10 | androidTestImplementation("androidx.test:runner:${Versions.Test.runner}")
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/di/module/ActivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.di.module
2 |
3 | import com.worker8.simplecurrency.di.scope.PerActivityScope
4 | import com.worker8.simplecurrency.ui.main.MainActivity
5 | import com.worker8.simplecurrency.ui.picker.PickerActivity
6 | import dagger.Module
7 | import dagger.android.ContributesAndroidInjector
8 |
9 | @Module
10 | abstract class ActivityModule {
11 |
12 | @PerActivityScope
13 | @ContributesAndroidInjector
14 | abstract fun contributeMainActivity(): MainActivity
15 |
16 | @PerActivityScope
17 | @ContributesAndroidInjector
18 | abstract fun contributePickerActivity(): PickerActivity
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_swap_button.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/main/MainRepoInterface.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.main
2 |
3 | import io.reactivex.Flowable
4 | import io.reactivex.Observable
5 | import io.reactivex.Scheduler
6 |
7 | interface MainRepoInterface {
8 | val mainThread: Scheduler
9 | val backgroundThread: Scheduler
10 | fun populateDbIfFirstTime(): Observable
11 | fun getSelectedBaseCurrencyCode(): String
12 | fun getSelectedTargetCurrencyCode(): String
13 | fun setSelectedBaseCurrencyCode(currencyCode: String): Boolean
14 | fun setSelectedTargetCurrencyCode(currencyCode: String): Boolean
15 | fun getLatestSelectedRateFlowable(): Flowable
16 | fun setupPeriodicUpdate()
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/SimpleCurrencyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency
2 |
3 | import com.jakewharton.threetenabp.AndroidThreeTen
4 | import com.worker8.simplecurrency.di.DaggerAppComponent
5 | import dagger.android.AndroidInjector
6 | import dagger.android.DaggerApplication
7 |
8 | class SimpleCurrencyApplication : DaggerApplication() {
9 | override fun onCreate() {
10 | super.onCreate()
11 |
12 | AndroidThreeTen.init(this)
13 | DebugSetting.init(this)
14 | }
15 |
16 | override fun applicationInjector(): AndroidInjector {
17 | return DaggerAppComponent
18 | .builder()
19 | .application(this)
20 | .build()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/db/entity/RoomConversionRate.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.db.entity
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import com.worker8.fixerio.model.ConversionRate
6 | import com.worker8.fixerio.model.Currency
7 |
8 | @Entity
9 | data class RoomConversionRate(
10 | @PrimaryKey
11 | val code: String, val rate: Double, val name: String
12 | ) {
13 | companion object {
14 | fun fromConversionRate(conversionRate: ConversionRate): RoomConversionRate {
15 | return RoomConversionRate(
16 | conversionRate.code,
17 | conversionRate.rate,
18 | Currency.ALL[conversionRate.code] ?: ""
19 | )
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/fixerio/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/db/SimpleCurrencyDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.worker8.simplecurrency.db.dao.RoomConversionRateDao
6 | import com.worker8.simplecurrency.db.dao.RoomUpdatedTimeStampDao
7 | import com.worker8.simplecurrency.db.entity.RoomConversionRate
8 | import com.worker8.simplecurrency.db.entity.RoomUpdatedTimeStamp
9 |
10 | @Database(
11 | entities = [RoomConversionRate::class, RoomUpdatedTimeStamp::class],
12 | version = 1,
13 | exportSchema = false /* TODO: export schema when the app is launched */
14 | )
15 | abstract class SimpleCurrencyDatabase : RoomDatabase() {
16 | abstract fun roomConversionRateDao(): RoomConversionRateDao
17 | abstract fun roomUpdatedTimeStampDao(): RoomUpdatedTimeStampDao
18 | }
19 |
--------------------------------------------------------------------------------
/fixerio/src/androidTest/java/com/worker8/fixerio/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.worker8.fixerio.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/network/SeedFixerIOLiveService.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.network
2 |
3 | import com.squareup.moshi.Moshi
4 | import com.worker8.fixerio.model.Rates
5 | import com.worker8.fixerio.response.EuroCurrencyResponse
6 | import java.io.BufferedReader
7 |
8 | class SeedFixerIOLiveService(val moshi: Moshi) {
9 | private val json: String by lazy {
10 | val inputStream =
11 | javaClass.classLoader.getResourceAsStream("usd_based_currencies_fixerio.json")
12 | inputStream.bufferedReader().use(BufferedReader::readText)
13 | }
14 |
15 | fun getSeedCurrencies(): Pair {
16 | val jsonAdapter = moshi.adapter(EuroCurrencyResponse::class.java)
17 | val response = jsonAdapter.fromJson(json)
18 | val rates = response?.rates ?: Rates()
19 | val timestamp = response?.timestamp ?: 0
20 | return rates to timestamp
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 | android.enableJetifier=true
17 | android.useAndroidX=true
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_logo_background.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/fixerio/src/test/java/com/worker8/fixerio/FixerIOLiveServiceTest.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio
2 |
3 | import com.worker8.fixerio.network.FixerIOLiveService
4 | import com.worker8.fixerio.network.FixerIORetrofit
5 | import org.junit.Assert
6 | import org.junit.Before
7 | import org.junit.Test
8 | import retrofit2.Retrofit
9 |
10 | class FixerIOLiveServiceTest {
11 | private lateinit var service: FixerIOLiveService
12 | private lateinit var retrofit: Retrofit
13 |
14 | @Before
15 | fun setup() {
16 | retrofit = FixerIORetrofit.build()
17 | service = retrofit.create(FixerIOLiveService::class.java)
18 | }
19 |
20 | @Test
21 | fun testCall() {
22 | val response = service.getCurrencies().blockingGet()
23 | response?.let {
24 | System.out.println(it)
25 | it.rates.conversionRates.forEach {
26 | System.out.println(it)
27 | }
28 | }
29 | Assert.assertNotNull(response)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/network/FixerIORetrofit.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.network
2 |
3 | import com.squareup.moshi.Moshi
4 | import com.worker8.fixerio.BuildConfig
5 | import okhttp3.OkHttpClient
6 | import retrofit2.Retrofit
7 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
8 | import retrofit2.converter.moshi.MoshiConverterFactory
9 |
10 |
11 | class FixerIORetrofit {
12 | companion object {
13 | private const val BaseUrl = BuildConfig.CURRENCY_API_URL;
14 |
15 | fun build(
16 | moshi: Moshi = FixerIOMoshi.build(),
17 | okHttpClient: OkHttpClient =
18 | OkHttpClient.Builder().build()
19 | ) =
20 | Retrofit.Builder()
21 | .baseUrl(BaseUrl)
22 | .client(okHttpClient)
23 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
24 | .addConverterFactory(MoshiConverterFactory.create(moshi))
25 | .build()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/di/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.di
2 |
3 | import com.worker8.simplecurrency.SimpleCurrencyApplication
4 | import com.worker8.simplecurrency.di.module.ActivityModule
5 | import com.worker8.simplecurrency.di.module.AppModule
6 | import com.worker8.simplecurrency.di.module.RepoModule
7 | import com.worker8.simplecurrency.worker.UpdateCurrencyWorker
8 | import dagger.BindsInstance
9 | import dagger.Component
10 | import dagger.android.AndroidInjector
11 | import dagger.android.support.AndroidSupportInjectionModule
12 | import javax.inject.Singleton
13 |
14 | @Singleton
15 | @Component(
16 | modules = [
17 | AndroidSupportInjectionModule::class,
18 | AppModule::class,
19 | ActivityModule::class,
20 | RepoModule::class
21 | ]
22 | )
23 | interface AppComponent : AndroidInjector {
24 | fun inject(updateCurrencyWorker: UpdateCurrencyWorker)
25 |
26 | @Component.Builder
27 | abstract class Builder {
28 | @BindsInstance
29 | abstract fun application(application: SimpleCurrencyApplication): Builder
30 |
31 | abstract fun build(): AppComponent
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/common/extension/DoubleExtension.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.common.extension
2 |
3 | import java.math.RoundingMode
4 | import java.text.DecimalFormat
5 | import java.text.DecimalFormatSymbols
6 | import java.util.*
7 |
8 | fun Double.toComma(): String {
9 | val decimalFormat = DecimalFormat("#,###.#######################")
10 | decimalFormat.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
11 | val bigDecimal = this.toBigDecimal()
12 | return decimalFormat.format(bigDecimal)
13 | }
14 |
15 | fun Double.toTwoDecimalWithComma(): String {
16 | val decimalFormat = DecimalFormat("#,###.##")
17 | decimalFormat.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
18 | val bigDecimal = this.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
19 | return decimalFormat.format(bigDecimal)
20 | }
21 |
22 | fun Double.toThreeDecimalWithComma(): String {
23 | val decimalFormat = DecimalFormat("#,###.###")
24 | decimalFormat.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
25 | val bigDecimal = this.toBigDecimal().setScale(3, RoundingMode.HALF_UP)
26 | return decimalFormat.format(bigDecimal)
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/main/event/BackSpaceInputEvent.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.main.event
2 |
3 | import com.worker8.simplecurrency.common.realValue
4 | import com.worker8.simplecurrency.ui.main.MainContract
5 | import io.reactivex.Observable
6 | import io.reactivex.subjects.BehaviorSubject
7 |
8 | class BackSpaceInputEvent(
9 | private val input: MainContract.Input,
10 | private val screenStateSubject: BehaviorSubject
11 | ) {
12 | private val currentScreenState get() = screenStateSubject.realValue
13 | fun process(): Observable {
14 | return input.backSpaceClick.map {
15 | if (currentInputString().isEmpty() || currentInputString() == "0") {
16 | currentInputString()
17 | } else if (currentInputString().length == 1) {
18 | "0"
19 | } else {
20 | currentInputString().removeRange(
21 | currentInputString().length - 1,
22 | currentInputString().length
23 | )
24 | }
25 | }.share()
26 | }
27 |
28 | private fun currentInputString(): String {
29 | return currentScreenState.inputNumberStringState
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/common/sharedPreference/MainPreference.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.common.sharedPreference
2 |
3 | import android.content.Context
4 |
5 | class MainPreference {
6 | companion object {
7 | private const val FIRST_TIME = "FIRST_TIME"
8 | private const val BASE_CURRENCY = "BASE_CURRENCY"
9 | private const val TARGET_CURRENCY = "TARGET_CURRENCY"
10 |
11 | fun setFirstTimeFalse(context: Context) =
12 | context.defaultPrefs().save(FIRST_TIME, false)
13 |
14 | fun getFirstTime(context: Context) =
15 | context.defaultPrefs().get(FIRST_TIME, true)
16 |
17 | fun setSelectedBaseCurrencyCode(context: Context, currencyCode: String) =
18 | context.defaultPrefs().save(BASE_CURRENCY, currencyCode)
19 |
20 | fun getSelectedBaseCurrencyCode(context: Context) =
21 | context.defaultPrefs().get(BASE_CURRENCY, "JPY")
22 |
23 | fun setSelectedTargetCurrencyCode(context: Context, currencyCode: String) =
24 | context.defaultPrefs().save(TARGET_CURRENCY, currencyCode)
25 |
26 | fun getSelectedTargetCurrencyCode(context: Context) =
27 | context.defaultPrefs().get(TARGET_CURRENCY, "EUR")
28 |
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/db/dao/RoomConversionRateDao.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.db.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.worker8.simplecurrency.db.entity.RoomConversionRate
8 | import io.reactivex.Flowable
9 |
10 | @Dao
11 | interface RoomConversionRateDao : BaseDao {
12 | @Insert(onConflict = OnConflictStrategy.REPLACE)
13 | fun insert(list: List)
14 |
15 | @Query("SELECT * FROM RoomConversionRate WHERE code=:currencyCode ORDER BY code ASC")
16 | fun findConversionRate(currencyCode: String): List
17 |
18 | @Query("SELECT * FROM RoomConversionRate")
19 | fun getRoomConversionRateList(): List
20 |
21 | @Query("SELECT * FROM RoomConversionRate WHERE code like :currencyCode OR name like :currenyName ORDER BY code ASC")
22 | fun findRoomConversionRateFlowable(
23 | currencyCode: String,
24 | currenyName: String
25 | ): Flowable>
26 |
27 | @Query("SELECT * FROM RoomConversionRate WHERE code=:currencyCode ORDER BY code ASC")
28 | fun findConversionRateFlowable(currencyCode: String): Flowable>
29 | }
30 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/adapter/QuotesCustomAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.adapter
2 |
3 | import com.squareup.moshi.*
4 | import com.worker8.fixerio.model.ConversionRate
5 | import com.worker8.fixerio.model.Quotes
6 |
7 | class QuotesCustomAdapter : JsonAdapter() {
8 | override fun fromJson(reader: JsonReader): Quotes? {
9 | val list = mutableListOf()
10 |
11 | reader.beginObject()
12 | while (reader.hasNext()) {
13 | list.add(
14 | ConversionRate(
15 | code = reader.nextName(),
16 | rate = reader.nextString().toDouble()
17 | )
18 | )
19 | }
20 | reader.endObject()
21 | return Quotes().apply {
22 | conversionRates = list
23 | }
24 | }
25 |
26 | override fun toJson(writer: JsonWriter, value: Quotes?) {
27 | //TODO: write this if needed
28 | }
29 | }
30 |
31 | //class ConversionsFactory : JsonAdapter.Factory {
32 | // override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? {
33 | // if (Types.getRawType(type).isAssignableFrom(Quotes::class.java)) {
34 | // return QuotesCustomAdapter()
35 | // }
36 | // return null
37 | // }
38 | //
39 | //}
40 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/adapter/RatesCustomAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.adapter
2 |
3 | import com.squareup.moshi.*
4 | import com.worker8.fixerio.model.ConversionRate
5 | import com.worker8.fixerio.model.Rates
6 | import java.lang.reflect.Type
7 |
8 | class RatesCustomAdapter : JsonAdapter() {
9 | override fun fromJson(reader: JsonReader): Rates? {
10 | val list = mutableListOf()
11 |
12 | reader.beginObject()
13 | while (reader.hasNext()) {
14 | list.add(
15 | ConversionRate(
16 | code = reader.nextName(),
17 | rate = reader.nextString().toDouble()
18 | )
19 | )
20 | }
21 | reader.endObject()
22 | return Rates().apply {
23 | conversionRates = list
24 | }
25 | }
26 |
27 | override fun toJson(writer: JsonWriter, value: Rates?) {
28 | //TODO: write this if needed
29 | }
30 | }
31 |
32 | class ConversionsFactory : JsonAdapter.Factory {
33 | override fun create(
34 | type: Type,
35 | annotations: MutableSet,
36 | moshi: Moshi
37 | ): JsonAdapter<*>? {
38 | if (Types.getRawType(type).isAssignableFrom(Rates::class.java)) {
39 | return RatesCustomAdapter()
40 | }
41 | return null
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/worker/UpdateCurrencyWorker.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.worker
2 |
3 | import android.content.Context
4 | import androidx.work.Worker
5 | import androidx.work.WorkerParameters
6 | import com.worker8.fixerio.network.FixerIOLiveService
7 | import com.worker8.simplecurrency.SimpleCurrencyApplication
8 | import com.worker8.simplecurrency.db.SimpleCurrencyDatabase
9 | import com.worker8.simplecurrency.db.entity.RoomConversionRate
10 | import com.worker8.simplecurrency.db.entity.RoomUpdatedTimeStamp
11 | import com.worker8.simplecurrency.di.DaggerAppComponent
12 | import javax.inject.Inject
13 |
14 | class UpdateCurrencyWorker(appContext: Context, workerParams: WorkerParameters) :
15 | Worker(appContext, workerParams) {
16 | @Inject
17 | lateinit var service: FixerIOLiveService
18 | @Inject
19 | lateinit var db: SimpleCurrencyDatabase
20 |
21 | override fun doWork(): Result {
22 | DaggerAppComponent.builder()
23 | .application(applicationContext as SimpleCurrencyApplication)
24 | .build()
25 | .inject(this)
26 |
27 | val response = service.getCurrencies()
28 | .blockingGet()
29 | val roomConversionRateList = response.rates.conversionRates.map {
30 | RoomConversionRate.fromConversionRate(it)
31 | }
32 | db.roomConversionRateDao().insert(roomConversionRateList)
33 | db.roomUpdatedTimeStampDao()
34 | .insert(RoomUpdatedTimeStamp("1", response.timestamp))
35 |
36 | return Result.success()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/picker/PickerViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.picker
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.worker8.simplecurrency.R
8 | import kotlinx.android.synthetic.main.row_picker.view.*
9 |
10 | class PickerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
11 |
12 | fun bind(
13 | pickerRowType: PickerAdapter.PickerRowType,
14 | isBase: Boolean,
15 | callback: (String) -> Unit
16 | ) {
17 | itemView.apply {
18 | pickerCurrencyName.text = pickerRowType.currencyName
19 | pickerCurrencyRate.text = pickerRowType.currencyRate
20 | pickerCurrencyCode.text = pickerRowType.currencyCode
21 | pickerCurrencyRateCalculated.text = pickerRowType.currencyRateCalculated
22 | val rateVisibility = if (isBase) {
23 | View.GONE
24 | } else {
25 | View.VISIBLE
26 | }
27 | pickerCurrencyRate.visibility = rateVisibility
28 | pickerCurrencyRateCalculated.visibility = rateVisibility
29 | setOnClickListener { callback.invoke(pickerRowType.currencyCode) }
30 | }
31 | }
32 |
33 | companion object {
34 | fun create(parent: ViewGroup) =
35 | LayoutInflater.from(parent.context)
36 | .inflate(R.layout.row_picker, parent, false)
37 | .let { PickerViewHolder(it) }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/main/MainContract.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.main
2 |
3 | import io.reactivex.Observable
4 |
5 | class MainContract {
6 | interface Input {
7 | val onNumpad0Click: Observable
8 | val onNumpad1Click: Observable
9 | val onNumpad2Click: Observable
10 | val onNumpad3Click: Observable
11 | val onNumpad4Click: Observable
12 | val onNumpad5Click: Observable
13 | val onNumpad6Click: Observable
14 | val onNumpad7Click: Observable
15 | val onNumpad8Click: Observable
16 | val onNumpad9Click: Observable
17 | val backSpaceClick: Observable
18 | val backSpaceLongClick: Observable
19 | val swapButtonClick: Observable
20 | val dotClick: Observable
21 | val onBaseCurrencyChanged: Observable
22 | val onTargetCurrencyChanged: Observable
23 | val onTargetCurrencyClicked: Observable
24 | }
25 |
26 | interface ViewAction {
27 | fun navigateToSelectTargetCurrency(inputAmount: Double)
28 | /* show this in a case of unrecoverable error */
29 | fun showTerminalError()
30 | }
31 |
32 | data class ScreenState(
33 | val inputNumberStringState: String = "0",
34 | val inputNumberString: String = "0",
35 | val outputNumberString: String = "0",
36 | val baseCurrencyCode: String = "",
37 | val targetCurrencyCode: String = "",
38 | val isEnableDot: Boolean = true
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/fixerio/src/test/java/com/worker8/fixerio/FixerIOMoshiTest.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio
2 |
3 | import com.squareup.moshi.Moshi
4 | import com.worker8.fixerio.model.Currency
5 | import com.worker8.fixerio.network.FixerIOMoshi
6 | import com.worker8.fixerio.response.EuroCurrencyResponse
7 | import org.junit.Assert
8 | import org.junit.Before
9 | import org.junit.Test
10 | import java.io.BufferedReader
11 |
12 | class FixerIOMoshiTest {
13 | private lateinit var moshi: Moshi
14 | private val json: String by lazy {
15 | val inputStream =
16 | javaClass.classLoader.getResourceAsStream("usd_based_currencies_fixerio.json")
17 | inputStream.bufferedReader().use(BufferedReader::readText)
18 | }
19 |
20 | @Before
21 | fun setup() {
22 | moshi = FixerIOMoshi.build()
23 | }
24 |
25 | @Test
26 | fun testCall() {
27 | val jsonAdapter = moshi.adapter(EuroCurrencyResponse::class.java)
28 | val response = jsonAdapter.fromJson(json)
29 | response?.let {
30 | System.out.println(it)
31 | it.rates.conversionRates.forEach {
32 | System.out.println(it)
33 | }
34 | }
35 | Assert.assertNotNull(response)
36 | }
37 |
38 | @Test
39 | fun testTest() {
40 | System.out.println("Currency.ALL.count: ${Currency.ALL.count()}")
41 | System.out.println("Currency.ALL.TEMP: ${Currency.TEMP.count()}")
42 | var count = 0
43 | Currency.ALL.forEach { (code, name) ->
44 | if (Currency.TEMP.contains(code)) {
45 | count++
46 | }
47 | }
48 |
49 | System.out.println("Total match: ${count}")
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/common/sharedPreference/PreferenceUtil.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.common.sharedPreference
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.preference.PreferenceManager
6 |
7 | fun Context.defaultPrefs(): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
8 | fun Context.customPrefs(name: String): SharedPreferences = getSharedPreferences(name, Context.MODE_PRIVATE)
9 |
10 | fun SharedPreferences.save(key: String, value: Any): Boolean {
11 | return edit().apply {
12 | when (value) {
13 | is String -> putString(key, value)
14 | is Int -> putInt(key, value)
15 | is Boolean -> putBoolean(key, value)
16 | is Float -> putFloat(key, value)
17 | is Long -> putLong(key, value)
18 | else -> throw UnsupportedOperationException("Unsupported Type")
19 | }
20 | }.commit()
21 | }
22 |
23 | inline fun SharedPreferences.get(key: String, defaultValue: T): T {
24 | defaultValue.ofType {
25 | return getString(key, it) as T
26 | }
27 |
28 | defaultValue.ofType {
29 | return getInt(key, it) as T
30 | }
31 |
32 | defaultValue.ofType {
33 | return getBoolean(key, it) as T
34 | }
35 |
36 | defaultValue.ofType {
37 | return getFloat(key, it) as T
38 | }
39 |
40 | defaultValue.ofType {
41 | return getLong(key, it) as T
42 | }
43 |
44 | throw UnsupportedOperationException("Unsupported Type")
45 | }
46 |
47 | inline fun Any.ofType(block: (T) -> Unit) {
48 | if (this is T) {
49 | block(this as T)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_logo_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
19 |
25 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/picker/PickerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.picker
2 |
3 | import android.view.ViewGroup
4 | import androidx.recyclerview.widget.DiffUtil
5 | import androidx.recyclerview.widget.ListAdapter
6 | import androidx.recyclerview.widget.RecyclerView
7 | import io.reactivex.Observable
8 | import io.reactivex.subjects.PublishSubject
9 |
10 | class PickerAdapter(private val isBase: Boolean) :
11 | ListAdapter(comparator) {
12 | private val clickSubject: PublishSubject = PublishSubject.create()
13 | val selectedCurrencyCode: Observable = clickSubject.hide()
14 |
15 | init {
16 | setHasStableIds(true)
17 | }
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
20 | return PickerViewHolder.create(parent)
21 | }
22 |
23 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
24 | val pickerRowType = getItem(position)
25 | (holder as PickerViewHolder).bind(pickerRowType, isBase) {
26 | clickSubject.onNext(it)
27 | }
28 | }
29 |
30 | override fun getItemId(position: Int) = getItem(position).currencyCode.hashCode().toLong()
31 |
32 | companion object {
33 | val comparator = object : DiffUtil.ItemCallback() {
34 | override fun areItemsTheSame(oldItem: PickerRowType, newItem: PickerRowType): Boolean {
35 | return oldItem.currencyCode == newItem.currencyCode
36 | }
37 |
38 | override fun areContentsTheSame(
39 | oldItem: PickerRowType,
40 | newItem: PickerRowType
41 | ): Boolean {
42 | return oldItem == newItem
43 | }
44 | }
45 | }
46 |
47 | data class PickerRowType(
48 | val currencyCode: String,
49 | val currencyName: String,
50 | val currencyRate: String,
51 | val currencyRateCalculated: String
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/main/event/NewNumberInputEvent.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.main.event
2 |
3 | import com.worker8.simplecurrency.common.realValue
4 | import com.worker8.simplecurrency.ui.main.MainContract
5 | import io.reactivex.Observable
6 | import io.reactivex.subjects.BehaviorSubject
7 |
8 | class NewNumberInputEvent(
9 | private val input: MainContract.Input,
10 | private val screenStateSubject: BehaviorSubject
11 | ) {
12 | private val currentScreenState get() = screenStateSubject.realValue
13 | fun process(): Observable {
14 | return Observable.merge(
15 | arrayListOf(
16 | input.onNumpad0Click,
17 | input.onNumpad1Click,
18 | input.onNumpad2Click,
19 | input.onNumpad3Click,
20 | input.onNumpad4Click,
21 | input.onNumpad5Click,
22 | input.onNumpad6Click,
23 | input.onNumpad7Click,
24 | input.onNumpad8Click,
25 | input.onNumpad9Click,
26 | input.dotClick
27 | )
28 | )
29 | .map { newChar ->
30 | if (newChar == '.') {
31 | // handling '.' as input
32 | // before: "0" --> after: "0."
33 | // before: "123 --> after: "123.
34 | currentInputString() + newChar
35 | } else if (currentInputString().length == 1 && currentInputString() == "0") {
36 | // handle case when input is "0" (beginning)
37 | // before: "0" --> after: "2"
38 | newChar.toString()
39 | } else {
40 | // handle normal case
41 | // before "123", after "1234"
42 | currentInputString() + newChar
43 | }
44 | }
45 | .share()
46 | }
47 |
48 | private fun currentInputString(): String {
49 | return currentScreenState.inputNumberStringState
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/main/event/CalculateConversionRateEvent.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.main.event
2 |
3 | import com.worker8.simplecurrency.ui.main.MainRepoInterface
4 | import io.reactivex.Observable
5 | import io.reactivex.functions.BiFunction
6 | import io.reactivex.subjects.PublishSubject
7 |
8 | class CalculateConversionRateEvent(
9 | private val newNumberInputSharedObservable: Observable,
10 | private val backSpaceInputEventSharedObservable: Observable,
11 | private val backSpaceLongClickSharedObservable: Observable,
12 | private val triggerCalculateSubject: PublishSubject = PublishSubject.create(),
13 | private val seedDatabaseSharedObservable: Observable,
14 | private val repo: MainRepoInterface
15 | ) {
16 | fun process(): Observable>> {
17 | return Observable.combineLatest(
18 | Observable.merge(
19 | newNumberInputSharedObservable,
20 | backSpaceInputEventSharedObservable,
21 | triggerCalculateSubject,
22 | backSpaceLongClickSharedObservable
23 | ),
24 | seedDatabaseSharedObservable.subscribeOn(repo.backgroundThread)
25 | .flatMap { repo.getLatestSelectedRateFlowable().toObservable() },
26 | BiFunction>> { numberString, rate ->
27 | val dotRemoved = if (numberString.isNotEmpty() && numberString.last() == '.') {
28 | numberString.removeRange(
29 | numberString.length - 1,
30 | numberString.length
31 | )
32 | } else {
33 | numberString
34 | }
35 | val input = dotRemoved.toDoubleOrNull()
36 | return@BiFunction if (input != null) {
37 | Result.success(input to rate)
38 | } else {
39 | Result.failure(Exception())
40 | }
41 | }).share()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
32 |
33 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/worker8/simplecurrency/RoomConversionRateDaoTest.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import androidx.test.core.app.ApplicationProvider
6 | import androidx.test.ext.junit.runners.AndroidJUnit4
7 | import com.worker8.simplecurrency.db.SimpleCurrencyDatabase
8 | import com.worker8.simplecurrency.db.dao.RoomConversionRateDao
9 | import com.worker8.simplecurrency.db.entity.RoomConversionRate
10 | import org.junit.Assert
11 | import org.junit.Before
12 | import org.junit.Test
13 | import org.junit.runner.RunWith
14 |
15 | @RunWith(AndroidJUnit4::class)
16 | class RoomConversionRateDaoTest {
17 | private lateinit var roomConversionRateDao: RoomConversionRateDao
18 | private lateinit var db: SimpleCurrencyDatabase
19 |
20 | @Before
21 | fun createDb() {
22 | val context = ApplicationProvider.getApplicationContext()
23 | db = Room.inMemoryDatabaseBuilder(context, SimpleCurrencyDatabase::class.java)
24 | .allowMainThreadQueries()
25 | .build()
26 | roomConversionRateDao = db.roomConversionRateDao()
27 | }
28 |
29 | @Test
30 | fun crudTest() {
31 | roomConversionRateDao.insert(
32 | RoomConversionRate("USDJPY", 106.25984, "Japanese Yen"),
33 | RoomConversionRate("USDKES", 103.910315, "Kenyan Shilling"),
34 | RoomConversionRate("USDKGS", 69.85001, "Kyrgystani Som"),
35 | RoomConversionRate("USDKHR", 4089.999831, "Cambodian Riel"),
36 | RoomConversionRate("USDKMF", 447.050295, "Comorian Franc")
37 | )
38 |
39 | val list = roomConversionRateDao.getRoomConversionRateList()
40 | Assert.assertNotNull(list)
41 | Assert.assertEquals(5, list.size)
42 |
43 | roomConversionRateDao.delete(RoomConversionRate("USDJPY", 106.25984, "Japanese Yen"))
44 | val deletedList = roomConversionRateDao.getRoomConversionRateList()
45 | Assert.assertEquals(4, deletedList.size)
46 |
47 | val foundList = roomConversionRateDao.findConversionRate("USDKMF")
48 | Assert.assertEquals(1, foundList.size)
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/picker/PickerRepo.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.picker
2 |
3 | import android.content.Context
4 | import com.worker8.simplecurrency.common.sharedPreference.MainPreference
5 | import com.worker8.simplecurrency.db.SimpleCurrencyDatabase
6 | import com.worker8.simplecurrency.db.entity.RoomConversionRate
7 | import com.worker8.simplecurrency.di.scope.PerActivityScope
8 | import com.worker8.simplecurrency.di.scope.ScopeConstant
9 | import io.reactivex.Observable
10 | import io.reactivex.Scheduler
11 | import org.threeten.bp.Instant
12 | import org.threeten.bp.ZoneId
13 | import org.threeten.bp.ZonedDateTime
14 | import org.threeten.bp.format.DateTimeFormatter
15 | import javax.inject.Inject
16 | import javax.inject.Named
17 |
18 | @PerActivityScope
19 | class PickerRepo @Inject constructor(
20 | private val context: Context,
21 | val db: SimpleCurrencyDatabase,
22 | @Named(ScopeConstant.MainThreadScheduler)
23 | val mainThread: Scheduler,
24 | @Named(ScopeConstant.BackgroundThreadScheduler)
25 | val backgroundThread: Scheduler
26 | ) {
27 | fun getAllCurrenciesFromDb(searchText: String): Observable> =
28 | db.roomConversionRateDao().findRoomConversionRateFlowable(
29 | "%$searchText%",
30 | "%$searchText%"
31 | ).toObservable()
32 |
33 | fun getBaseRate(): List {
34 | val baseCurrency = getSelectedBaseCurrencyCode()
35 | return db.roomConversionRateDao().findConversionRate("$baseCurrency")
36 | }
37 |
38 | fun getLatestUpdatedDate(): Observable {
39 | return Observable.fromCallable {
40 | val foundList = db.roomUpdatedTimeStampDao().findLatestTimeStamp()
41 |
42 | if (foundList.isNotEmpty()) {
43 | val formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm a")
44 | val zonedDateTime = ZonedDateTime.ofInstant(
45 | Instant.ofEpochSecond(foundList[0].timeStamp),
46 | ZoneId.systemDefault()
47 | )
48 | zonedDateTime.format(formatter)
49 | } else ""
50 | }
51 | }
52 |
53 | fun getSelectedBaseCurrencyCode() =
54 | MainPreference.getSelectedBaseCurrencyCode(context)
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/row_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
31 |
32 |
41 |
42 |
51 |
52 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/di/module/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.di.module
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import androidx.work.WorkManager
6 | import com.squareup.moshi.Moshi
7 | import com.worker8.fixerio.network.FixerIOLiveService
8 | import com.worker8.fixerio.network.FixerIOMoshi
9 | import com.worker8.fixerio.network.FixerIORetrofit
10 | import com.worker8.simplecurrency.SimpleCurrencyApplication
11 | import com.worker8.simplecurrency.db.SimpleCurrencyDatabase
12 | import com.worker8.simplecurrency.di.scope.ScopeConstant
13 | import com.worker8.simplecurrency.provideOkHttpClient
14 | import dagger.Binds
15 | import dagger.Module
16 | import dagger.Provides
17 | import io.reactivex.Scheduler
18 | import io.reactivex.android.schedulers.AndroidSchedulers
19 | import io.reactivex.schedulers.Schedulers
20 | import okhttp3.OkHttpClient
21 | import retrofit2.Retrofit
22 | import javax.inject.Named
23 | import javax.inject.Singleton
24 |
25 | @Module(includes = [AppModule.AppModuleInterface::class])
26 | class AppModule {
27 |
28 | @Singleton
29 | @Provides
30 | fun provideDatabase(context: Context): SimpleCurrencyDatabase {
31 | return Room.databaseBuilder(
32 | context,
33 | SimpleCurrencyDatabase::class.java,
34 | "SimpleCurrencyDatabase"
35 | ).build()
36 | }
37 |
38 | @Singleton
39 | @Provides
40 | fun provideMoshi(): Moshi {
41 | return FixerIOMoshi.build()
42 | }
43 |
44 | @Singleton
45 | @Provides
46 | fun provideOKHttp3(): OkHttpClient {
47 | return provideOkHttpClient()
48 | }
49 |
50 | @Singleton
51 | @Provides
52 | fun provideRetrofit(moshi: Moshi, okHttpClient: OkHttpClient): Retrofit {
53 | return FixerIORetrofit.build(moshi, okHttpClient)
54 | }
55 |
56 | @Singleton
57 | @Provides
58 | fun provideWorkManager(context: Context): WorkManager {
59 | return WorkManager.getInstance(context)
60 | }
61 |
62 | @Singleton
63 | @Provides
64 | fun provideFixerIOLiveService(retrofit: Retrofit): FixerIOLiveService {
65 | return retrofit.create(FixerIOLiveService::class.java)
66 | }
67 |
68 | @Named(ScopeConstant.MainThreadScheduler)
69 | @Provides
70 | fun provideMainThreadScheduler(): Scheduler {
71 | return AndroidSchedulers.mainThread()
72 | }
73 |
74 | @Named(ScopeConstant.BackgroundThreadScheduler)
75 | @Provides
76 | fun provideBackgroundThreadScheduler(): Scheduler {
77 | return Schedulers.io()
78 | }
79 |
80 | @Module
81 | interface AppModuleInterface {
82 | @Binds
83 | fun provideContext(application: SimpleCurrencyApplication): Context
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | android {
2 | defaultConfig {
3 | applicationId = "com.worker8.simplecurrency"
4 | }
5 | dependencies {
6 | implementation(project(":fixerio"))
7 |
8 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.Tool.kotlin}")
9 |
10 | /* Architecture */
11 | implementation("androidx.appcompat:appcompat:${Versions.App.androidX}")
12 | implementation("androidx.lifecycle:lifecycle-extensions:${Versions.App.viewModel}")
13 | implementation("androidx.lifecycle:lifecycle-viewmodel:${Versions.App.viewModel}")
14 | implementation("androidx.work:work-runtime:${Versions.App.workManager}")
15 | implementation("io.reactivex.rxjava2:rxjava:${Versions.App.rxJava}")
16 | implementation("io.reactivex.rxjava2:rxandroid:${Versions.App.rxAndroid}")
17 | implementation("com.jakewharton.rxbinding3:rxbinding:${Versions.App.rxBinding}")
18 | implementation("com.jakewharton.threetenabp:threetenabp:${Versions.App.threeTenABP}")
19 |
20 | /* UI */
21 | implementation("androidx.constraintlayout:constraintlayout:${Versions.App.constraintLayout}")
22 | implementation("com.google.android.material:material:${Versions.App.material}")
23 |
24 | /* Network & Data Layer */
25 | implementation("com.squareup.retrofit2:retrofit:${Versions.App.retrofit}")
26 | implementation("com.squareup.retrofit2:converter-moshi:${Versions.App.retrofit}")
27 | implementation("androidx.room:room-runtime:${Versions.App.room}")
28 | implementation("androidx.room:room-rxjava2:${Versions.App.room}")
29 | annotationProcessor("androidx.room:room-compiler:${Versions.App.room}")
30 | kapt("androidx.room:room-compiler:${Versions.App.room}")
31 |
32 | /* DI */
33 | implementation("com.google.dagger:dagger:${Versions.App.dagger}")
34 | kapt("com.google.dagger:dagger-compiler:${Versions.App.dagger}")
35 | implementation("com.google.dagger:dagger-android:${Versions.App.dagger}")
36 | implementation("com.google.dagger:dagger-android-support:${Versions.App.dagger}")
37 | kapt("com.google.dagger:dagger-android-processor:${Versions.App.dagger}")
38 |
39 | /* Debug */
40 | debugImplementation("com.facebook.stetho:stetho:${Versions.Debug.stetho}")
41 | debugImplementation("com.facebook.stetho:stetho-okhttp3:${Versions.Debug.stetho}")
42 |
43 | /* Test */
44 | testImplementation("junit:junit:${Versions.Test.junit}")
45 | testImplementation("io.mockk:mockk:${Versions.Test.mockk}")
46 | androidTestImplementation("androidx.test.ext:junit:${Versions.Test.junitExt}")
47 | androidTestImplementation("androidx.test:core:${Versions.Test.core}")
48 | androidTestImplementation("androidx.test:runner:${Versions.Test.runner}")
49 | androidTestImplementation("androidx.test.espresso:espresso-core:${Versions.Test.espresso}")
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/picker/PickerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.picker
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.lifecycle.ViewModelProviders
6 | import com.google.android.material.snackbar.Snackbar
7 | import com.jakewharton.rxbinding3.widget.textChanges
8 | import com.worker8.simplecurrency.R
9 | import com.worker8.simplecurrency.common.addTo
10 | import dagger.android.support.DaggerAppCompatActivity
11 | import io.reactivex.disposables.CompositeDisposable
12 | import kotlinx.android.synthetic.main.activity_main.*
13 | import kotlinx.android.synthetic.main.activity_picker.*
14 | import javax.inject.Inject
15 |
16 | class PickerActivity : DaggerAppCompatActivity() {
17 | @Inject
18 | lateinit var repo: PickerRepo
19 |
20 | private val adapter by lazy { PickerAdapter(isBase) }
21 | private val disposableBag = CompositeDisposable()
22 |
23 | val isBase get() = intent.getBooleanExtra(BASE_OR_TARGET_KEY, true)
24 | val inputAmount get() = intent.getDoubleExtra(INPUT_AMOUNT, 0.0)
25 |
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 | setContentView(R.layout.activity_picker)
29 |
30 | val inputLocal = object : PickerContract.Input {
31 | override val inputAmount = this@PickerActivity.inputAmount
32 | override val onFilterTextChanged =
33 | pickerInput.textChanges().map { it.toString() }
34 | override val isBase = this@PickerActivity.isBase
35 | }
36 | val viewActionLocal = object : PickerContract.ViewAction {
37 | override fun showTerminalError() {
38 | Snackbar.make(pickerContainer, R.string.error_message, Snackbar.LENGTH_INDEFINITE)
39 | .show()
40 | }
41 | }
42 | pickerRecyclerView.adapter = adapter
43 | val viewModel =
44 | ViewModelProviders.of(this, PickerViewModel.PickerViewModelFactory(repo))
45 | .get(PickerViewModel::class.java)
46 | .apply {
47 | input = inputLocal
48 | viewAction = viewActionLocal
49 | }
50 |
51 | viewModel.screenState
52 | .distinctUntilChanged()
53 | .observeOn(repo.mainThread)
54 | .subscribe({ screenState ->
55 | screenState.apply {
56 | adapter.submitList(currencyList.toList())
57 | pickerLastUpdatedMessage.text =
58 | "${getString(R.string.last_updated)} ${latestUpdatedString}"
59 | }
60 | }, {
61 | viewActionLocal.showTerminalError()
62 | })
63 | .addTo(disposableBag)
64 |
65 | adapter.selectedCurrencyCode
66 | .subscribe({ currencyCode ->
67 | setResult(RESULT_OK, Intent().apply { putExtra(RESULT_KEY, currencyCode) })
68 | finish()
69 | }
70 | , {
71 | viewActionLocal.showTerminalError()
72 | })
73 | .addTo(disposableBag)
74 | lifecycle.addObserver(viewModel)
75 | }
76 |
77 | override fun onDestroy() {
78 | super.onDestroy()
79 | disposableBag.dispose()
80 | }
81 |
82 | companion object {
83 | const val INPUT_AMOUNT = "INPUT_AMOUNT"
84 | const val BASE_OR_TARGET_KEY = "BASE_OR_TARGET_KEY"
85 | const val RESULT_KEY = "RESULT_KEY"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/picker/PickerViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.picker
2 |
3 | import androidx.lifecycle.*
4 | import com.worker8.fixerio.model.Currency
5 | import com.worker8.simplecurrency.common.addTo
6 | import com.worker8.simplecurrency.common.extension.toThreeDecimalWithComma
7 | import com.worker8.simplecurrency.common.extension.toTwoDecimalWithComma
8 | import com.worker8.simplecurrency.common.realValue
9 | import io.reactivex.Observable
10 | import io.reactivex.disposables.CompositeDisposable
11 | import io.reactivex.subjects.BehaviorSubject
12 | import java.util.concurrent.TimeUnit
13 |
14 | class PickerViewModel(private val repo: PickerRepo) :
15 | ViewModel(), LifecycleObserver {
16 | private val screenStateSubject =
17 | BehaviorSubject.createDefault(PickerContract.ScreenState(linkedSetOf(), false, ""))
18 | private val currentScreenState: PickerContract.ScreenState get() = screenStateSubject.realValue
19 | var screenState: Observable =
20 | screenStateSubject.hide().observeOn(repo.mainThread)
21 | private val disposableBag = CompositeDisposable()
22 | lateinit var input: PickerContract.Input
23 | lateinit var viewAction: PickerContract.ViewAction
24 | @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
25 | fun onCreate() {
26 | input.apply {
27 | Observable.merge(
28 | Observable.just(""),
29 | onFilterTextChanged.debounce(300, TimeUnit.MILLISECONDS)
30 | )
31 | .flatMap { repo.getAllCurrenciesFromDb(it) }
32 | .subscribeOn(repo.backgroundThread)
33 | .observeOn(repo.backgroundThread)
34 | .map { it to repo.getBaseRate() }
35 | .map { (filteredCurrencyRates, baseCurrencyList) ->
36 | val baseCurrency = baseCurrencyList[0].rate
37 | val resultSet = linkedSetOf()
38 | filteredCurrencyRates.forEach { roomConversionRate ->
39 | resultSet.add(roomConversionRate.run {
40 | val baseToTargetRate = (rate / baseCurrency)
41 | PickerAdapter.PickerRowType(
42 | currencyName = Currency.ALL[code] ?: "",
43 | currencyRate = "1 ${repo.getSelectedBaseCurrencyCode()} = ${baseToTargetRate.toThreeDecimalWithComma()} ${code}",
44 | currencyRateCalculated = "${inputAmount.toTwoDecimalWithComma()} ${repo.getSelectedBaseCurrencyCode()} = ${(inputAmount * baseToTargetRate).toTwoDecimalWithComma()} ${code}",
45 | currencyCode = code
46 | )
47 | })
48 | }
49 | resultSet
50 | }
51 | .subscribe({
52 | dispatch(currentScreenState.copy(currencyList = it))
53 | }, {
54 | it.printStackTrace()
55 | viewAction.showTerminalError()
56 | })
57 | .addTo(disposableBag)
58 |
59 | repo.getLatestUpdatedDate()
60 | .subscribeOn(repo.backgroundThread)
61 | .observeOn(repo.mainThread)
62 | .subscribe({
63 | dispatch(
64 | currentScreenState.copy(
65 | rateDetailVisibility = !isBase,
66 | latestUpdatedString = it
67 | )
68 | )
69 | }, {
70 | viewAction.showTerminalError()
71 | })
72 | .addTo(disposableBag)
73 |
74 | }
75 | }
76 |
77 | private fun dispatch(screenState: PickerContract.ScreenState) {
78 | screenStateSubject.onNext(screenState)
79 | }
80 |
81 | override fun onCleared() {
82 | super.onCleared()
83 | disposableBag.dispose()
84 | }
85 |
86 | @Suppress("UNCHECKED_CAST")
87 | class PickerViewModelFactory(private val repo: PickerRepo) :
88 | ViewModelProvider.NewInstanceFactory() {
89 | override fun create(modelClass: Class): T {
90 | return PickerViewModel(repo) as T
91 | }
92 | }
93 | }
94 |
95 | //TODO: style arrangement
96 |
--------------------------------------------------------------------------------
/fixerio/src/main/resources/usd_based_currencies_fixerio.json:
--------------------------------------------------------------------------------
1 | {
2 | "success": true,
3 | "timestamp": 1583288406,
4 | "base": "EUR",
5 | "date": "2020-03-04",
6 | "rates": {
7 | "AED": 4.101566,
8 | "AFN": 84.028139,
9 | "ALL": 122.857268,
10 | "AMD": 532.613407,
11 | "ANG": 1.989356,
12 | "AOA": 549.252648,
13 | "ARS": 69.471865,
14 | "AUD": 1.692605,
15 | "AWG": 2.009916,
16 | "AZN": 1.90059,
17 | "BAM": 1.955769,
18 | "BBD": 2.24395,
19 | "BDT": 94.40276,
20 | "BGN": 1.953554,
21 | "BHD": 0.420538,
22 | "BIF": 2096.613471,
23 | "BMD": 1.11662,
24 | "BND": 1.547566,
25 | "BOB": 7.67376,
26 | "BRL": 5.041319,
27 | "BSD": 1.111375,
28 | "BTC": 0.000127,
29 | "BTN": 81.372356,
30 | "BWP": 12.389625,
31 | "BYN": 2.482845,
32 | "BYR": 21885.756324,
33 | "BZD": 2.24015,
34 | "CAD": 1.492307,
35 | "CDF": 1896.021647,
36 | "CHF": 1.067997,
37 | "CLF": 0.032981,
38 | "CLP": 910.152717,
39 | "CNY": 7.77458,
40 | "COP": 3865.739204,
41 | "CRC": 635.965888,
42 | "CUC": 1.11662,
43 | "CUP": 29.590436,
44 | "CVE": 110.261568,
45 | "CZK": 25.411263,
46 | "DJF": 198.445397,
47 | "DKK": 7.472813,
48 | "DOP": 59.390681,
49 | "DZD": 133.61137,
50 | "EGP": 17.474102,
51 | "ERN": 16.749029,
52 | "ETB": 36.251996,
53 | "EUR": 1,
54 | "FJD": 2.472141,
55 | "FKP": 0.870986,
56 | "GBP": 0.870981,
57 | "GEL": 3.115615,
58 | "GGP": 0.870986,
59 | "GHS": 6.034612,
60 | "GIP": 0.870986,
61 | "GMD": 56.947954,
62 | "GNF": 10589.464466,
63 | "GTQ": 8.535133,
64 | "GYD": 231.892776,
65 | "HKD": 8.673995,
66 | "HNL": 27.400392,
67 | "HRK": 7.484673,
68 | "HTG": 102.155732,
69 | "HUF": 336.16952,
70 | "IDR": 15793.253076,
71 | "ILS": 3.843239,
72 | "IMP": 0.870986,
73 | "INR": 81.781133,
74 | "IQD": 1326.728618,
75 | "IRR": 47015.294142,
76 | "ISK": 142.212856,
77 | "JEP": 0.870986,
78 | "JMD": 151.065649,
79 | "JOD": 0.791668,
80 | "JPY": 119.737438,
81 | "KES": 114.352905,
82 | "KGS": 77.996395,
83 | "KHR": 4523.799569,
84 | "KMF": 498.403361,
85 | "KPW": 1004.996568,
86 | "KRW": 1326.567419,
87 | "KWD": 0.341572,
88 | "KYD": 0.926171,
89 | "KZT": 422.896828,
90 | "LAK": 9875.392593,
91 | "LBP": 1680.56268,
92 | "LKR": 202.375099,
93 | "LRD": 220.546546,
94 | "LSL": 17.430825,
95 | "LTL": 3.297089,
96 | "LVL": 0.675433,
97 | "LYD": 1.560765,
98 | "MAD": 10.633569,
99 | "MDL": 19.420869,
100 | "MGA": 4118.668531,
101 | "MKD": 61.673079,
102 | "MMK": 1587.564073,
103 | "MNT": 3090.113538,
104 | "MOP": 8.905522,
105 | "MRO": 398.633458,
106 | "MUR": 41.815251,
107 | "MVR": 17.30648,
108 | "MWK": 816.053868,
109 | "MXN": 21.676669,
110 | "MYR": 4.675848,
111 | "MZN": 73.032501,
112 | "NAD": 17.430163,
113 | "NGN": 408.129425,
114 | "NIO": 37.491168,
115 | "NOK": 10.353864,
116 | "NPR": 130.197103,
117 | "NZD": 1.778452,
118 | "OMR": 0.42995,
119 | "PAB": 1.111365,
120 | "PEN": 3.819681,
121 | "PGK": 3.836215,
122 | "PHP": 56.601444,
123 | "PKR": 171.364694,
124 | "PLN": 4.307815,
125 | "PYG": 7246.939149,
126 | "QAR": 4.065571,
127 | "RON": 4.808386,
128 | "RSD": 117.546233,
129 | "RUB": 73.908316,
130 | "RWF": 1057.606507,
131 | "SAR": 4.188432,
132 | "SBD": 9.536153,
133 | "SCR": 15.303716,
134 | "SDG": 61.693561,
135 | "SEK": 10.559308,
136 | "SGD": 1.550494,
137 | "SHP": 0.870986,
138 | "SLL": 10831.215552,
139 | "SOS": 654.339137,
140 | "SRD": 8.327728,
141 | "STD": 24441.45232,
142 | "SVC": 9.724697,
143 | "SYP": 575.058096,
144 | "SZL": 17.294816,
145 | "THB": 35.060201,
146 | "TJS": 10.758865,
147 | "TMT": 3.919337,
148 | "TND": 3.157243,
149 | "TOP": 2.599937,
150 | "TRY": 6.830702,
151 | "TTD": 7.514566,
152 | "TWD": 33.466783,
153 | "TZS": 2574.473537,
154 | "UAH": 27.761884,
155 | "UGX": 4115.271773,
156 | "USD": 1.11662,
157 | "UYU": 43.525645,
158 | "UZS": 10558.841214,
159 | "VEF": 11.152248,
160 | "VND": 25804.427792,
161 | "VUV": 132.973628,
162 | "WST": 3.020235,
163 | "XAF": 655.942447,
164 | "XAG": 0.064782,
165 | "XAU": 0.000679,
166 | "XCD": 3.017722,
167 | "XDR": 0.805882,
168 | "XOF": 655.942446,
169 | "XPF": 119.25629,
170 | "YER": 279.491007,
171 | "ZAR": 17.211528,
172 | "ZMK": 10050.923204,
173 | "ZMW": 16.975924,
174 | "ZWL": 359.551712
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/main/MainRepo.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.main
2 |
3 | import android.content.Context
4 | import androidx.work.*
5 | import com.squareup.moshi.Moshi
6 | import com.worker8.fixerio.network.SeedFixerIOLiveService
7 | import com.worker8.simplecurrency.common.sharedPreference.MainPreference
8 | import com.worker8.simplecurrency.db.SimpleCurrencyDatabase
9 | import com.worker8.simplecurrency.db.entity.RoomConversionRate
10 | import com.worker8.simplecurrency.db.entity.RoomUpdatedTimeStamp
11 | import com.worker8.simplecurrency.di.scope.ScopeConstant
12 | import com.worker8.simplecurrency.worker.UpdateCurrencyWorker
13 | import io.reactivex.Flowable
14 | import io.reactivex.Observable
15 | import io.reactivex.Scheduler
16 | import io.reactivex.functions.BiFunction
17 | import java.util.concurrent.TimeUnit
18 | import javax.inject.Inject
19 | import javax.inject.Named
20 |
21 | class MainRepo @Inject constructor(
22 | private val context: Context,
23 | val db: SimpleCurrencyDatabase,
24 | private val moshi: Moshi,
25 | private val workManager: WorkManager,
26 | @Named(ScopeConstant.MainThreadScheduler)
27 | override val mainThread: Scheduler,
28 | @Named(ScopeConstant.BackgroundThreadScheduler)
29 | override val backgroundThread: Scheduler
30 | ) : MainRepoInterface {
31 |
32 | override fun populateDbIfFirstTime(): Observable {
33 | return Observable.fromCallable {
34 | if (MainPreference.getFirstTime(context)) {
35 | // populate
36 | val (quotes, timestamp) = SeedFixerIOLiveService(moshi).getSeedCurrencies()
37 | val roomConversionRateList = quotes.conversionRates.map {
38 | RoomConversionRate.fromConversionRate(it)
39 | }
40 | db.roomConversionRateDao().insert(roomConversionRateList)
41 | db.roomUpdatedTimeStampDao().insert(RoomUpdatedTimeStamp("1", timestamp))
42 | return@fromCallable MainPreference.setFirstTimeFalse(context)
43 | } else {
44 | return@fromCallable true
45 | }
46 | }
47 | }
48 |
49 | override fun getSelectedBaseCurrencyCode() =
50 | MainPreference.getSelectedBaseCurrencyCode(context)
51 |
52 | override fun getSelectedTargetCurrencyCode() =
53 | MainPreference.getSelectedTargetCurrencyCode(context)
54 |
55 | override fun setSelectedBaseCurrencyCode(currencyCode: String) =
56 | MainPreference.setSelectedBaseCurrencyCode(context, currencyCode)
57 |
58 | override fun setSelectedTargetCurrencyCode(currencyCode: String) =
59 | MainPreference.setSelectedTargetCurrencyCode(context, currencyCode)
60 |
61 | private fun getBaseRateFlowable(): Flowable> {
62 | val baseCurrency = getSelectedBaseCurrencyCode() // "JPY"
63 | return db.roomConversionRateDao().findConversionRateFlowable("$baseCurrency")
64 | }
65 |
66 | private fun getTargetRateFlowable(): Flowable> {
67 | val targetCurrency = getSelectedTargetCurrencyCode() // "JPY"
68 | return db.roomConversionRateDao().findConversionRateFlowable("$targetCurrency")
69 | }
70 |
71 | override fun getLatestSelectedRateFlowable(): Flowable {
72 | return Flowable.combineLatest(
73 | getBaseRateFlowable(),
74 | getTargetRateFlowable(),
75 | BiFunction, List, Double> { baseRateList, targetRateList ->
76 | return@BiFunction if (baseRateList.isNotEmpty() && targetRateList.isNotEmpty()) {
77 | targetRateList.first().rate / baseRateList.first().rate
78 | } else {
79 | -1.0
80 | }
81 | })
82 | }
83 |
84 | override fun setupPeriodicUpdate() {
85 | val constraints = Constraints.Builder()
86 | .setRequiredNetworkType(NetworkType.CONNECTED).build()
87 | val updateCurrencyWorker =
88 | PeriodicWorkRequest.Builder(UpdateCurrencyWorker::class.java, 30, TimeUnit.MINUTES)
89 | .setConstraints(constraints)
90 | .build()
91 | workManager.enqueueUniquePeriodicWork(
92 | uniqueWorkerName,
93 | ExistingPeriodicWorkPolicy.KEEP,
94 | updateCurrencyWorker
95 | )
96 |
97 | /* uncomment the following for testing purpose */
98 | // val oneTimeCurrencyWorker = OneTimeWorkRequest.Builder(UpdateCurrencyWorker::class.java)
99 | // .setConstraints(constraints)
100 | // .build()
101 | // workManager.enqueue(oneTimeCurrencyWorker)
102 | }
103 |
104 | companion object {
105 | const val uniqueWorkerName = "get_latest_currency"
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/privacy_policy.md:
--------------------------------------------------------------------------------
1 | ## Privacy Policy
2 |
3 | Tan Jun Rong built the Simple Currency app as an Open Source app. This service is provided by Tan Jun Rong at no cost and is intended for use as is.
4 |
5 | This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.
6 |
7 | If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy.
8 |
9 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Simple Currency unless otherwise defined in this Privacy Policy.
10 |
11 | **Information Collection and Use**
12 |
13 | For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request will be retained on your device and is not collected by me in any way.
14 |
15 | The app does use third party services that may collect information used to identify you.
16 |
17 | Link to privacy policy of third party service providers used by the app
18 |
19 | * [Google Play Services](https://www.google.com/policies/privacy/)
20 | * [Firebase Analytics](https://firebase.google.com/policies/analytics)
21 | * [Firebase Crashlytics](https://firebase.google.com/terms/crashlytics)
22 |
23 | **Log Data**
24 |
25 | I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.
26 |
27 | **Cookies**
28 |
29 | Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
30 |
31 | This Service does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service.
32 |
33 | **Service Providers**
34 |
35 | I may employ third-party companies and individuals due to the following reasons:
36 |
37 | * To facilitate our Service;
38 | * To provide the Service on our behalf;
39 | * To perform Service-related services; or
40 | * To assist us in analyzing how our Service is used.
41 |
42 | I want to inform users of this Service that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.
43 |
44 | **Security**
45 |
46 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.
47 |
48 | **Links to Other Sites**
49 |
50 | This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
51 |
52 | **Children’s Privacy**
53 |
54 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13\. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do necessary actions.
55 |
56 | **Changes to This Privacy Policy**
57 |
58 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
59 |
60 | **Contact Us**
61 |
62 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at BeepBeep Software (beepbeepsoftware@gmail.com).
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.main
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.lifecycle.ViewModelProviders
7 | import com.google.android.material.snackbar.Snackbar
8 | import com.jakewharton.rxbinding3.view.clicks
9 | import com.jakewharton.rxbinding3.view.longClicks
10 | import com.worker8.simplecurrency.R
11 | import com.worker8.simplecurrency.common.addTo
12 | import com.worker8.simplecurrency.ui.picker.PickerActivity
13 | import dagger.android.support.DaggerAppCompatActivity
14 | import io.reactivex.disposables.CompositeDisposable
15 | import io.reactivex.subjects.PublishSubject
16 | import kotlinx.android.synthetic.main.activity_main.*
17 | import kotlinx.android.synthetic.main.numpad.*
18 | import javax.inject.Inject
19 |
20 | class MainActivity : DaggerAppCompatActivity() {
21 | private val disposableBag = CompositeDisposable()
22 |
23 | private val onBaseCurrencyChangedSubject: PublishSubject = PublishSubject.create()
24 | private val onTargetCurrencyChangedSubject: PublishSubject = PublishSubject.create()
25 |
26 | @Inject
27 | lateinit var repo: MainRepoInterface
28 |
29 | private val PICKER_BASE_REQUEST_CODE = 3832
30 | val PICKER_TARGET_REQUEST_CODE = 3833
31 |
32 | override fun onCreate(savedInstanceState: Bundle?) {
33 | super.onCreate(savedInstanceState)
34 | setContentView(R.layout.activity_main)
35 |
36 | val mainInput = object : MainContract.Input {
37 | override val swapButtonClick = mainFab.clicks()
38 | override val onBaseCurrencyChanged = onBaseCurrencyChangedSubject.hide()
39 | override val onTargetCurrencyChanged = onTargetCurrencyChangedSubject.hide()
40 | override val onNumpad0Click by lazy { mainNum0.clicks().map { '0' } }
41 | override val onNumpad1Click by lazy { mainNum1.clicks().map { '1' } }
42 | override val onNumpad2Click by lazy { mainNum2.clicks().map { '2' } }
43 | override val onNumpad3Click by lazy { mainNum3.clicks().map { '3' } }
44 | override val onNumpad4Click by lazy { mainNum4.clicks().map { '4' } }
45 | override val onNumpad5Click by lazy { mainNum5.clicks().map { '5' } }
46 | override val onNumpad6Click by lazy { mainNum6.clicks().map { '6' } }
47 | override val onNumpad7Click by lazy { mainNum7.clicks().map { '7' } }
48 | override val onNumpad8Click by lazy { mainNum8.clicks().map { '8' } }
49 | override val onNumpad9Click by lazy { mainNum9.clicks().map { '9' } }
50 | override val backSpaceClick by lazy { mainNumBackspace.clicks() }
51 | override val dotClick by lazy { mainNumDot.clicks().map { '.' } }
52 | override val onTargetCurrencyClicked by lazy { mainTargetCurrencyPicker.clicks() }
53 | override val backSpaceLongClick = mainNumBackspace.longClicks()
54 | }
55 | val viewActionLocal = object : MainContract.ViewAction {
56 | override fun navigateToSelectTargetCurrency(inputAmount: Double) {
57 | val intent = Intent(this@MainActivity, PickerActivity::class.java)
58 | .apply {
59 | putExtra(PickerActivity.BASE_OR_TARGET_KEY, false)
60 | putExtra(PickerActivity.INPUT_AMOUNT, inputAmount)
61 | }
62 | startActivityForResult(intent, PICKER_TARGET_REQUEST_CODE)
63 | }
64 |
65 | override fun showTerminalError() {
66 | Snackbar.make(mainContainer, R.string.error_message, Snackbar.LENGTH_INDEFINITE)
67 | .show()
68 | }
69 | }
70 | val viewModel =
71 | ViewModelProviders.of(this, MainViewModel.MainViewModelFactory(repo))
72 | .get(MainViewModel::class.java).apply {
73 | input = mainInput
74 | viewAction = viewActionLocal
75 | }
76 | lifecycle.addObserver(viewModel)
77 | mainBaseCurrencyPicker.setOnClickListener {
78 | val intent = Intent(this@MainActivity, PickerActivity::class.java)
79 | .apply { putExtra(PickerActivity.BASE_OR_TARGET_KEY, true) }
80 | startActivityForResult(intent, PICKER_BASE_REQUEST_CODE)
81 | }
82 | viewModel.screenState
83 | .distinctUntilChanged()
84 | .observeOn(repo.mainThread)
85 | .subscribe({
86 | it.apply {
87 | mainInputCurrency.text = baseCurrencyCode
88 | mainOutputCurrency.text = targetCurrencyCode
89 | mainInputNumber.text = inputNumberString
90 | mainOutputNumber.text = outputNumberString
91 | mainNumDot.isEnabled = isEnableDot
92 | }
93 | }, {
94 | viewActionLocal.showTerminalError()
95 | })
96 | .addTo(disposableBag)
97 |
98 | }
99 |
100 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
101 | super.onActivityResult(requestCode, resultCode, data)
102 | if (resultCode == Activity.RESULT_OK) {
103 | when (requestCode) {
104 | PICKER_BASE_REQUEST_CODE -> {
105 | data?.getStringExtra(PickerActivity.RESULT_KEY)?.let {
106 | onBaseCurrencyChangedSubject.onNext(it)
107 | }
108 | }
109 | PICKER_TARGET_REQUEST_CODE -> {
110 | data?.getStringExtra(PickerActivity.RESULT_KEY)?.let {
111 | onTargetCurrencyChangedSubject.onNext(it)
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 | override fun onDestroy() {
119 | super.onDestroy()
120 | disposableBag.dispose()
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/worker8/simplecurrency/ui/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency.ui.main
2 |
3 | import androidx.lifecycle.*
4 | import com.worker8.simplecurrency.common.addTo
5 | import com.worker8.simplecurrency.common.extension.toTwoDecimalWithComma
6 | import com.worker8.simplecurrency.common.realValue
7 | import com.worker8.simplecurrency.common.util.NumberFormatterUtil
8 | import com.worker8.simplecurrency.ui.main.event.BackSpaceInputEvent
9 | import com.worker8.simplecurrency.ui.main.event.CalculateConversionRateEvent
10 | import com.worker8.simplecurrency.ui.main.event.NewNumberInputEvent
11 | import io.reactivex.Observable
12 | import io.reactivex.disposables.CompositeDisposable
13 | import io.reactivex.functions.BiFunction
14 | import io.reactivex.subjects.BehaviorSubject
15 | import io.reactivex.subjects.PublishSubject
16 |
17 | class MainViewModel(private val repo: MainRepoInterface) :
18 | ViewModel(), LifecycleObserver {
19 | private val screenStateSubject = BehaviorSubject.createDefault(MainContract.ScreenState())
20 | private val disposableBag = CompositeDisposable()
21 | private val currentScreenState: MainContract.ScreenState get() = screenStateSubject.realValue
22 | var screenState: Observable = screenStateSubject.hide()
23 |
24 | lateinit var input: MainContract.Input
25 | lateinit var viewAction: MainContract.ViewAction
26 | private lateinit var seedDatabaseSharedObservable: Observable
27 | private lateinit var backSpaceLongClickSharedObservable: Observable
28 | private lateinit var newNumberInputSharedObservable: Observable
29 | private lateinit var backSpaceInputEventSharedObservable: Observable
30 | private lateinit var calculateConversionRateSharedObservable: Observable>>
31 | private lateinit var onTargetCurrencyClickedShared: Observable
32 | private val triggerCalculateSubject: PublishSubject = PublishSubject.create()
33 |
34 | @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
35 | fun onCreate() {
36 | setupInputEvents()
37 | processOutputEvents()
38 | triggerCalculateSubject.onNext(currentInputString())
39 | }
40 |
41 | private fun processOutputEvents() {
42 | Observable.merge(
43 | newNumberInputSharedObservable,
44 | backSpaceInputEventSharedObservable,
45 | backSpaceLongClickSharedObservable
46 | )
47 | .subscribe({ newInputString ->
48 | dispatch(
49 | currentScreenState.copy(
50 | inputNumberString = NumberFormatterUtil.addComma(newInputString),
51 | inputNumberStringState = newInputString,
52 | isEnableDot = !newInputString.contains(".")
53 | )
54 | )
55 | }, {
56 | viewAction.showTerminalError()
57 | })
58 | .addTo(disposableBag)
59 |
60 | input.onBaseCurrencyChanged
61 | .subscribe({
62 | repo.setSelectedBaseCurrencyCode(it)
63 | onCreate()
64 | }, {
65 | viewAction.showTerminalError()
66 | })
67 | .addTo(disposableBag)
68 |
69 | input.onTargetCurrencyChanged
70 | .subscribe({
71 | repo.setSelectedTargetCurrencyCode(it)
72 | onCreate()
73 | }, {
74 | viewAction.showTerminalError()
75 | })
76 | .addTo(disposableBag)
77 |
78 | calculateConversionRateSharedObservable
79 | .map { result ->
80 | val (input, rate) = result.getOrDefault(Pair(0.0, 0.0))
81 | input to input * rate
82 | }
83 | .subscribe({ (input, outputCurrency) ->
84 | dispatch(
85 | currentScreenState.copy(
86 | baseCurrencyCode = repo.getSelectedBaseCurrencyCode(),
87 | targetCurrencyCode = repo.getSelectedTargetCurrencyCode(),
88 | outputNumberString = outputCurrency.toTwoDecimalWithComma()
89 | )
90 | )
91 | }, {
92 | viewAction.showTerminalError()
93 | })
94 | .addTo(disposableBag)
95 |
96 | onTargetCurrencyClickedShared
97 | .subscribeOn(repo.mainThread)
98 | .withLatestFrom(calculateConversionRateSharedObservable,
99 | BiFunction>, Double> { _, result ->
100 | val (input, _) = result.getOrDefault(Pair(0.0, 0.0))
101 | input
102 | })
103 | .observeOn(repo.mainThread)
104 | .subscribe({ viewAction.navigateToSelectTargetCurrency(it) }, {
105 | viewAction.showTerminalError()
106 | })
107 | .addTo(disposableBag)
108 |
109 | input.swapButtonClick
110 | .subscribe({
111 | val tempBaseCode = repo.getSelectedBaseCurrencyCode()
112 | val tempTargetCode = repo.getSelectedTargetCurrencyCode()
113 | repo.setSelectedBaseCurrencyCode(tempTargetCode)
114 | repo.setSelectedTargetCurrencyCode(tempBaseCode)
115 | onCreate()
116 | }, {
117 | viewAction.showTerminalError()
118 | })
119 | .addTo(disposableBag)
120 |
121 | repo.setupPeriodicUpdate()
122 | }
123 |
124 | private fun setupInputEvents() {
125 | disposableBag.clear()
126 |
127 | onTargetCurrencyClickedShared = input.onTargetCurrencyClicked.share()
128 | backSpaceLongClickSharedObservable = input.backSpaceLongClick.map { "0" }.share()
129 | newNumberInputSharedObservable =
130 | NewNumberInputEvent(input, screenStateSubject).process()
131 | backSpaceInputEventSharedObservable =
132 | BackSpaceInputEvent(input, screenStateSubject).process()
133 | seedDatabaseSharedObservable = repo.populateDbIfFirstTime().share()
134 | calculateConversionRateSharedObservable = CalculateConversionRateEvent(
135 | newNumberInputSharedObservable = newNumberInputSharedObservable,
136 | backSpaceInputEventSharedObservable = backSpaceInputEventSharedObservable,
137 | backSpaceLongClickSharedObservable = backSpaceLongClickSharedObservable,
138 | triggerCalculateSubject = triggerCalculateSubject,
139 | seedDatabaseSharedObservable = seedDatabaseSharedObservable,
140 | repo = repo
141 | ).process()
142 | }
143 |
144 | private fun currentInputString(): String {
145 | return currentScreenState.inputNumberStringState
146 | }
147 |
148 | override fun onCleared() {
149 | super.onCleared()
150 | disposableBag.dispose()
151 | }
152 |
153 | private fun dispatch(screenState: MainContract.ScreenState) {
154 | screenStateSubject.onNext(screenState)
155 | }
156 |
157 | @Suppress("UNCHECKED_CAST")
158 | class MainViewModelFactory(private val repo: MainRepoInterface) :
159 | ViewModelProvider.NewInstanceFactory() {
160 | override fun create(modelClass: Class): T {
161 | return MainViewModel(repo) as T
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
29 |
30 |
42 |
43 |
52 |
53 |
63 |
64 |
80 |
81 |
93 |
94 |
106 |
107 |
117 |
118 |
127 |
128 |
144 |
145 |
153 |
154 |
160 |
161 |
167 |
168 |
174 |
175 |
181 |
182 |
190 |
191 |
192 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SimpleCurrency
2 |
3 |
4 |
5 | Simple Currency is an application that handles currency conversion from 168 countries.
6 |
7 | ## Playstore
8 | Get it in the Playstore:
9 |
10 |
11 |
12 | ## Demo Gif
13 | Here's a quick demo of how it works:
14 |
15 |
16 |
17 | click to show demo gif
18 |
19 |
20 |
21 |
22 |
23 | ## Demo Screenshots
24 | Here are some screenshots:
25 |
26 |
27 |
28 | click to show screenshots
29 |
30 |
31 | | HomeScreen | CurrencyScreen 1 | CurrencyScreen 2 | Landscape |
32 | | - | - | - | - |
33 | |
|
|
|
|
34 |
35 |
36 | ## Features
37 | - convert currency from one to another
38 | - support decimal point
39 | - long press on `'x'` will clear all input
40 | - swap button to switch between the base and target currency quickly
41 | - automatic currency rate updates every 30 minutes
42 | - pick from 168 currencies
43 | - filter currency by currency name or code in currency picker
44 | - comma seperation for big numbers
45 | - landscape mode supported
46 |
47 | ## How to Setup
48 | The API used for obtaining the latest currency data is [Fixer.io](https://fixer.io/). A free account can be made easily and it will provide an API key. You need to fill up the API key in `api_keys.properties` file at the root of this project. Instructions:
49 |
50 | 1. make a file named `api_keys.properties` in the root of the project
51 | 2. add this line in the file: `FIXER_IO_ACCESS_TOKEN=`
52 |
53 | This is how the file looks like:
54 |
55 | ```shell
56 | ༼つ◕_◕༽つ RootOfSimpleCurrency (master)$ cat api_keys.properties
57 | FIXER_IO_ACCESS_TOKEN=d0bbf06c7xxxxxxxxxxxx47d2e56ed6f
58 |
59 | ```
60 |
61 | ## Tech Explanation
62 | The following dependencies are used in this project:
63 | - RxJava
64 | - RxBinding
65 | - Dagger2
66 | - Retrofit
67 | - Moshi
68 | - Room
69 | - Android Arch Lifecycle
70 | - Android Arch ViewModel
71 | - WorkManager
72 | - Material Design Library
73 | - Mockk
74 | - JUnit
75 | - etc..
76 |
77 | ### Programming Language
78 | This project is written in Kotlin.
79 |
80 | ### Architecture
81 | MVVM is used in this app. The view layer is made reactive and passed into the `ViewModel` as the input. External sources that are needed (such as database access, network calls, shared preference access) will be passed into `ViewModel` as `Repo`. This way `ViewModel` doesn't have access to Android-related code so that it can be unit tested.
82 |
83 | Room Persistence library is used to access SQLite easily. This is used to store the currency data. `USD` is used as the base currency, so all the currency stored in the database is referenced against `USD`. Let's say we wanted to find out `Japanese Yen (JPY)` vs. `Pound Sterling (GBP)`, simple math calculation will be done.
84 |
85 | In every periodic interval (currently set at every 30 minutes), the `WorkManager` will fire up a Retrofit call to get the latest currency rate. The obtained json will be deserialized by Moshi and write into the database.
86 |
87 | The fixer io network library is extracted into a separate **module** so that it can be decoupled from the main app. It can be launched as a standalone separate network library or being swapped out and replaced by another currency API.
88 |
89 | ### Unidirectional data flow & Immutable data
90 | The project follows the unidirectional data flow rule to better structure the code.
91 |
92 | Here's a brief diagram of the main activity architecture.
93 |
94 |
95 | 1. RxView - The flow begins from the views. Every user view interactions are reactive and fed into the `ViewModel`.
96 | 2. Repo - Any external data access that is not from the user interactions, such as network calls, shared preference, database access, etc... will all go through the `Repo` class that is passed into `ViewModel`.
97 | 3. ScreenState - The reactive signals from `Views` will be processed inside `ViewModel` by some business logic. After that, it will produce the next `ScreenState` by using `.copy()` from Kotlin to ensure immutability.
98 | 4. View Update - Finally, all the views will listen to the `ScreenState` and update itself accordingly.
99 |
100 | This architecture is quite similar to [MVI](https://www.raywenderlich.com/817602-mvi-architecture-for-android-tutorial-getting-started). The difference is that it doesn't use reducer or model the input as 'intent'.
101 |
102 |
103 | ### Unit Test
104 | `ViewModel` doesn't have access to Android related code. Therefore, it can be tested by JUnit `test`, without using `androidTest`. Example Unit Test can be found [MainViewModelTest.kt](([MainViewModelTest.kt](https://github.com/worker8/SimpleCurrency/blob/master/app/src/test/java/com/worker8/simplecurrency/MainViewModelTest.kt))).
105 |
106 | The general idea of testing an Activity can be described by this diagram:
107 |
108 |
109 |
110 | The `RxViews` signals are replaced by fake inputs from the test. Reactive RxBinding signals are be replaced by RxJava's `Subjects` , reactive database access `Flowable` are replaced by RxJava's `Processors` and normal method calls are mocked by `mockk`. This way, user interactions can be controlled by us. After that, we can make `Assertion` on the `ScreenState` to check if it behaves correctly.
111 |
112 | Here's an example of testing a simple conversion ([MainViewModelTest.kt#L112](https://github.com/worker8/SimpleCurrency/blob/4f320e6d9cec77d78849f70d1f1ad1fa9b2dbdd8/app/src/test/java/com/worker8/simplecurrency/MainViewModelTest.kt#L112)):
113 |
114 | ```kotlin
115 | fun testSimpleConversion() {
116 | // 1. arrange
117 | val fakeRate = 2.0
118 | viewModel.onCreate()
119 | val screenStateTestObserver = viewModel.screenState.test()
120 |
121 | // 2. act
122 | populateDbIfFirstTime.onNext(true)
123 | getLatestSelectedRateFlowable.offer(fakeRate)
124 |
125 | onNumpad1Click.onNext('1')
126 | onNumpad0Click.onNext('0')
127 | onNumpad0Click.onNext('0')
128 |
129 | // 3. assert
130 | verify(exactly = 1) { repo.setupPeriodicUpdate() }
131 | screenStateTestObserver.assertNoErrors()
132 |
133 | screenStateTestObserver.lastValue.apply {
134 | Assert.assertEquals("200", outputNumberString)
135 | }
136 | }
137 | ```
138 |
139 | #### Description
140 | 1. Arrange - we first setup the necessary objects
141 |
142 | 2. Act - next, we make some actions.
143 |
144 | While we run the following:
145 |
146 | ```kotlin
147 | // 2. act
148 | populateDbIfFirstTime.onNext(true)
149 | getLatestSelectedRateFlowable.offer(fakeRate)
150 |
151 | onNumpad1Click.onNext('1')
152 | onNumpad0Click.onNext('0')
153 | onNumpad0Click.onNext('0')
154 | ```
155 |
156 | It is actually doing these:
157 |
158 | 1. `populateDbIfFirstTime.onNext(true)` - seeded the db
159 | 2. `getLatestSelectedRateFlowable.offer(fakeRate)` - taken the latest conversion rate from db
160 | 3. `onNumpad1Click.onNext('1')`, `onNumpad1Click.onNext('0')`, `onNumpad1Click.onNext('0')` - click on `1`, `0`, `0` (100)
161 |
162 | 3. Assert - finally, we check on the output:
163 | ```kotlin
164 | screenStateTestObserver.lastValue.apply {
165 | Assert.assertEquals("200", outputNumberString)
166 | }
167 | ```
168 | Since the fake conversaion rate is set to `2.0`, the output should be `200` when input is `100`.
169 |
170 | ## Debugging
171 | [Stetho](http://facebook.github.io/stetho/) library is used for debugging purpose. Network calls through OkHttp3 and SQLite Database can be viewed by accessing `chrome://inspect/#devices` on a chrome browser. This is an example of how the database look like in DevTools: [devtool db debug screenshot](https://user-images.githubusercontent.com/1988156/64595502-09678580-d3ed-11e9-9a08-659d21389d51.png).
172 |
173 |
174 | ## Adaptive Icon
175 | An adaptive icon is created using Sketch. The sketch file can be found in [`logo.sketch`](https://github.com/worker8/SimpleCurrency/blob/master/logo.sketch) file at the root of this project.
176 |
177 | ## Coding Style
178 | `.editorconfig` is used in this project to make sure that the spacing and indentations are standardized, the `editorconfig` is obtained from [ktlint project](https://github.com/shyiko/ktlint/blob/master/.editorconfig).
179 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
29 |
30 |
42 |
43 |
52 |
53 |
63 |
64 |
80 |
81 |
94 |
95 |
107 |
108 |
118 |
119 |
128 |
129 |
145 |
146 |
154 |
155 |
163 |
164 |
172 |
173 |
181 |
182 |
190 |
191 |
199 |
200 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/numpad.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
32 |
33 |
50 |
51 |
66 |
67 |
68 |
73 |
74 |
91 |
92 |
108 |
109 |
125 |
126 |
127 |
132 |
133 |
150 |
151 |
167 |
168 |
184 |
185 |
186 |
191 |
192 |
209 |
210 |
226 |
227 |
243 |
244 |
245 |
246 |
--------------------------------------------------------------------------------
/app/src/test/java/com/worker8/simplecurrency/MainViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.simplecurrency
2 |
3 | import com.worker8.simplecurrency.ui.main.MainContract
4 | import com.worker8.simplecurrency.ui.main.MainRepoInterface
5 | import com.worker8.simplecurrency.ui.main.MainViewModel
6 | import io.mockk.*
7 | import io.reactivex.observers.TestObserver
8 | import io.reactivex.processors.PublishProcessor
9 | import io.reactivex.schedulers.Schedulers
10 | import io.reactivex.subjects.PublishSubject
11 | import org.junit.Assert
12 | import org.junit.Before
13 | import org.junit.Test
14 |
15 | class MainViewModelTest {
16 | private lateinit var input: MainContract.Input
17 | private lateinit var repo: MainRepoInterface
18 | private lateinit var viewModel: MainViewModel
19 | private lateinit var viewAction: MainContract.ViewAction
20 |
21 | /* input */
22 | private lateinit var onNumpad0Click: PublishSubject
23 | private lateinit var onNumpad1Click: PublishSubject
24 | private lateinit var onNumpad2Click: PublishSubject
25 | private lateinit var onNumpad3Click: PublishSubject
26 | private lateinit var onNumpad4Click: PublishSubject
27 | private lateinit var onNumpad5Click: PublishSubject
28 | private lateinit var onNumpad6Click: PublishSubject
29 | private lateinit var onNumpad7Click: PublishSubject
30 | private lateinit var onNumpad8Click: PublishSubject
31 | private lateinit var onNumpad9Click: PublishSubject
32 | private lateinit var backSpaceClick: PublishSubject
33 | private lateinit var backSpaceLongClick: PublishSubject
34 | private lateinit var swapButtonClick: PublishSubject
35 | private lateinit var dotClick: PublishSubject
36 | private lateinit var onBaseCurrencyChanged: PublishSubject
37 | private lateinit var onTargetCurrencyChanged: PublishSubject
38 | private lateinit var onTargetCurrencyClicked: PublishSubject
39 |
40 | /* repo */
41 | private lateinit var getLatestSelectedRateFlowable: PublishProcessor
42 | private lateinit var populateDbIfFirstTime: PublishSubject
43 |
44 | @Before
45 | fun setup() {
46 | input = mockk()
47 |
48 | onNumpad0Click = PublishSubject.create()
49 | onNumpad1Click = PublishSubject.create()
50 | onNumpad2Click = PublishSubject.create()
51 | onNumpad3Click = PublishSubject.create()
52 | onNumpad4Click = PublishSubject.create()
53 | onNumpad5Click = PublishSubject.create()
54 | onNumpad6Click = PublishSubject.create()
55 | onNumpad7Click = PublishSubject.create()
56 | onNumpad8Click = PublishSubject.create()
57 | onNumpad9Click = PublishSubject.create()
58 | backSpaceClick = PublishSubject.create()
59 | backSpaceLongClick = PublishSubject.create()
60 | swapButtonClick = PublishSubject.create()
61 | dotClick = PublishSubject.create()
62 | onBaseCurrencyChanged = PublishSubject.create()
63 | onTargetCurrencyChanged = PublishSubject.create()
64 | onTargetCurrencyClicked = PublishSubject.create()
65 |
66 | getLatestSelectedRateFlowable = PublishProcessor.create()
67 | populateDbIfFirstTime = PublishSubject.create()
68 |
69 | every { input.onNumpad0Click } returns onNumpad0Click
70 | every { input.onNumpad1Click } returns onNumpad1Click
71 | every { input.onNumpad2Click } returns onNumpad2Click
72 | every { input.onNumpad3Click } returns onNumpad3Click
73 | every { input.onNumpad4Click } returns onNumpad4Click
74 | every { input.onNumpad5Click } returns onNumpad5Click
75 | every { input.onNumpad6Click } returns onNumpad6Click
76 | every { input.onNumpad7Click } returns onNumpad7Click
77 | every { input.onNumpad8Click } returns onNumpad8Click
78 | every { input.onNumpad9Click } returns onNumpad9Click
79 |
80 | every { input.backSpaceClick } returns backSpaceClick
81 | every { input.backSpaceLongClick } returns backSpaceLongClick
82 | every { input.swapButtonClick } returns swapButtonClick
83 | every { input.dotClick } returns dotClick
84 | every { input.onBaseCurrencyChanged } returns onBaseCurrencyChanged
85 | every { input.onTargetCurrencyChanged } returns onTargetCurrencyChanged
86 | every { input.onTargetCurrencyClicked } returns onTargetCurrencyClicked
87 |
88 | repo = mockk(relaxed = true)
89 |
90 | every { repo.mainThread } returns Schedulers.trampoline()
91 | every { repo.backgroundThread } returns Schedulers.trampoline()
92 | every { repo.populateDbIfFirstTime() } returns populateDbIfFirstTime
93 | every { repo.getSelectedBaseCurrencyCode() } returns "JPY"
94 | every { repo.getSelectedTargetCurrencyCode() } returns "USD"
95 |
96 | every { repo.setSelectedBaseCurrencyCode(any()) } returns true
97 | every { repo.setSelectedTargetCurrencyCode(any()) } returns true
98 |
99 | every { repo.getLatestSelectedRateFlowable() } returns getLatestSelectedRateFlowable
100 | every { repo.setupPeriodicUpdate() } just Runs
101 |
102 | viewAction = mockk(relaxed = true)
103 |
104 | viewModel = MainViewModel(repo)
105 | .apply {
106 | input = this@MainViewModelTest.input
107 | viewAction = this@MainViewModelTest.viewAction
108 | }
109 | }
110 |
111 | @Test
112 | fun testSimpleConversion() {
113 | // 1. arrange
114 | val fakeRate = 2.0
115 | viewModel.onCreate()
116 | val screenStateTestObserver = viewModel.screenState.test()
117 |
118 | // 2. act
119 | populateDbIfFirstTime.onNext(true)
120 | getLatestSelectedRateFlowable.offer(fakeRate)
121 |
122 | onNumpad1Click.onNext('1')
123 | onNumpad0Click.onNext('0')
124 | onNumpad0Click.onNext('0')
125 |
126 | // 3. assert
127 | verify(exactly = 1) { repo.setupPeriodicUpdate() }
128 | screenStateTestObserver.assertNoErrors()
129 |
130 | screenStateTestObserver.lastValue.apply {
131 | Assert.assertEquals("200", outputNumberString)
132 | }
133 | }
134 |
135 | @Test
136 | fun testDecimalConversion() {
137 | // 1. arrange
138 | val fakeRate = 2.0
139 | viewModel.onCreate()
140 | val screenStateTestObserver = viewModel.screenState.test()
141 |
142 | // 2. act
143 | populateDbIfFirstTime.onNext(true)
144 | getLatestSelectedRateFlowable.offer(fakeRate)
145 |
146 | // key in: 99.2
147 | onNumpad1Click.onNext('9')
148 | onNumpad0Click.onNext('9')
149 | onNumpad0Click.onNext('.')
150 | onNumpad0Click.onNext('2')
151 |
152 | // 3. assert
153 | verify(exactly = 1) { repo.setupPeriodicUpdate() }
154 | screenStateTestObserver.assertNoErrors()
155 |
156 | screenStateTestObserver.lastValue.apply {
157 | Assert.assertEquals("198.4", outputNumberString) // 99.2 * 2 = 198.4
158 | }
159 | }
160 |
161 | @Test
162 | fun testBigNumberWithCommaConversion() {
163 | // 1. arrange
164 | val fakeRate = 2.0
165 | viewModel.onCreate()
166 | val screenStateTestObserver = viewModel.screenState.test()
167 |
168 | // 2. act
169 | populateDbIfFirstTime.onNext(true)
170 | getLatestSelectedRateFlowable.offer(fakeRate)
171 |
172 | // key in: 1,000,123.2
173 | onNumpad1Click.onNext('1')
174 | onNumpad0Click.onNext('0')
175 | onNumpad0Click.onNext('0')
176 | onNumpad0Click.onNext('0')
177 | onNumpad0Click.onNext('1')
178 | onNumpad0Click.onNext('2')
179 | onNumpad0Click.onNext('3')
180 | onNumpad0Click.onNext('.')
181 | onNumpad0Click.onNext('2')
182 |
183 | // 3. assert
184 | verify(exactly = 1) { repo.setupPeriodicUpdate() }
185 | screenStateTestObserver.assertNoErrors()
186 |
187 | screenStateTestObserver.lastValue.apply {
188 | Assert.assertEquals("1,000,123.2", inputNumberString)
189 | Assert.assertEquals("2,000,246.4", outputNumberString) // 1,000,123.2 * 2
190 | }
191 | }
192 |
193 | @Test
194 | fun testBackSpaceLongPress() {
195 | // 1. arrange
196 | val fakeRate = 2.0
197 | viewModel.onCreate()
198 | val screenStateTestObserver = viewModel.screenState.test()
199 |
200 | // 2. act
201 | populateDbIfFirstTime.onNext(true)
202 | getLatestSelectedRateFlowable.offer(fakeRate)
203 |
204 | // key in: 1,000,123.2
205 | onNumpad1Click.onNext('1')
206 | onNumpad0Click.onNext('0')
207 | onNumpad0Click.onNext('0')
208 | onNumpad0Click.onNext('0')
209 | onNumpad0Click.onNext('0')
210 | onNumpad0Click.onNext('0')
211 |
212 | backSpaceClick.onNext(Unit)
213 |
214 | screenStateTestObserver.lastValue.apply {
215 | Assert.assertEquals("10,000", inputNumberString)
216 | Assert.assertEquals("20,000", outputNumberString)
217 | }
218 |
219 | backSpaceLongClick.onNext(Unit)
220 |
221 | // 3. assert
222 | screenStateTestObserver.assertNoErrors()
223 | screenStateTestObserver.lastValue.apply {
224 | Assert.assertEquals("0", inputNumberString)
225 | Assert.assertEquals("0", outputNumberString)
226 | }
227 | }
228 |
229 | @Test
230 | fun testSwapCurrency() {
231 | // 1. arrange
232 | val fakeRate = 2.0
233 | viewModel.onCreate()
234 | val screenStateTestObserver = viewModel.screenState.test()
235 |
236 | // 2. act
237 | populateDbIfFirstTime.onNext(true)
238 | getLatestSelectedRateFlowable.offer(fakeRate)
239 | onNumpad1Click.onNext('1')
240 | onNumpad0Click.onNext('0')
241 | onNumpad0Click.onNext('0')
242 |
243 | // 3. assert - check initial state
244 | screenStateTestObserver.lastValue.apply {
245 | Assert.assertEquals("JPY", baseCurrencyCode)
246 | Assert.assertEquals("USD", targetCurrencyCode) // 1,000,123.2 * 2
247 | }
248 |
249 | // 2. act - hit swap button
250 | verify(exactly = 4) {
251 | repo.getSelectedBaseCurrencyCode()
252 | }
253 |
254 | swapButtonClick.onNext(Unit)
255 | populateDbIfFirstTime.onNext(true)
256 | getLatestSelectedRateFlowable.offer(4.0) // not using real db here, update manually
257 |
258 | // 3. assert - check if output is updated once currency is updated
259 | screenStateTestObserver.lastValue.apply {
260 | Assert.assertEquals("100", inputNumberString)
261 | Assert.assertEquals("400", outputNumberString) // 1,000,123.2 * 2
262 | }
263 | }
264 |
265 | private val TestObserver.lastValue
266 | get() = this.values().last()
267 | }
268 |
--------------------------------------------------------------------------------
/fixerio/src/main/java/com/worker8/fixerio/model/Currency.kt:
--------------------------------------------------------------------------------
1 | package com.worker8.fixerio.model
2 |
3 | data class Currency(val code: String, val name: String) {
4 | companion object {
5 | val ALL_STRING: String
6 | get() {
7 | return ALL.keys.joinToString(",")
8 | }
9 | // TODO: remove TEMP and friends
10 | val TEMP = listOf(
11 | "AED",
12 | "AFN",
13 | "ALL",
14 | "AMD",
15 | "ANG",
16 | "AOA",
17 | "ARS",
18 | "AUD",
19 | "AWG",
20 | "AZN",
21 | "BAM",
22 | "BBD",
23 | "BDT",
24 | "BGN",
25 | "BHD",
26 | "BIF",
27 | "BMD",
28 | "BND",
29 | "BOB",
30 | "BRL",
31 | "BSD",
32 | "BTC",
33 | "BTN",
34 | "BWP",
35 | "BYN",
36 | "BYR",
37 | "BZD",
38 | "CAD",
39 | "CDF",
40 | "CHF",
41 | "CLF",
42 | "CLP",
43 | "CNY",
44 | "COP",
45 | "CRC",
46 | "CUC",
47 | "CUP",
48 | "CVE",
49 | "CZK",
50 | "DJF",
51 | "DKK",
52 | "DOP",
53 | "DZD",
54 | "EGP",
55 | "ERN",
56 | "ETB",
57 | "EUR",
58 | "FJD",
59 | "FKP",
60 | "GBP",
61 | "GEL",
62 | "GGP",
63 | "GHS",
64 | "GIP",
65 | "GMD",
66 | "GNF",
67 | "GTQ",
68 | "GYD",
69 | "HKD",
70 | "HNL",
71 | "HRK",
72 | "HTG",
73 | "HUF",
74 | "IDR",
75 | "ILS",
76 | "IMP",
77 | "INR",
78 | "IQD",
79 | "IRR",
80 | "ISK",
81 | "JEP",
82 | "JMD",
83 | "JOD",
84 | "JPY",
85 | "KES",
86 | "KGS",
87 | "KHR",
88 | "KMF",
89 | "KPW",
90 | "KRW",
91 | "KWD",
92 | "KYD",
93 | "KZT",
94 | "LAK",
95 | "LBP",
96 | "LKR",
97 | "LRD",
98 | "LSL",
99 | "LTL",
100 | "LVL",
101 | "LYD",
102 | "MAD",
103 | "MDL",
104 | "MGA",
105 | "MKD",
106 | "MMK",
107 | "MNT",
108 | "MOP",
109 | "MRO",
110 | "MUR",
111 | "MVR",
112 | "MWK",
113 | "MXN",
114 | "MYR",
115 | "MZN",
116 | "NAD",
117 | "NGN",
118 | "NIO",
119 | "NOK",
120 | "NPR",
121 | "NZD",
122 | "OMR",
123 | "PAB",
124 | "PEN",
125 | "PGK",
126 | "PHP",
127 | "PKR",
128 | "PLN",
129 | "PYG",
130 | "QAR",
131 | "RON",
132 | "RSD",
133 | "RUB",
134 | "RWF",
135 | "SAR",
136 | "SBD",
137 | "SCR",
138 | "SDG",
139 | "SEK",
140 | "SGD",
141 | "SHP",
142 | "SLL",
143 | "SOS",
144 | "SRD",
145 | "STD",
146 | "SVC",
147 | "SYP",
148 | "SZL",
149 | "THB",
150 | "TJS",
151 | "TMT",
152 | "TND",
153 | "TOP",
154 | "TRY",
155 | "TTD",
156 | "TWD",
157 | "TZS",
158 | "UAH",
159 | "UGX",
160 | "USD",
161 | "UYU",
162 | "UZS",
163 | "VEF",
164 | "VND",
165 | "VUV",
166 | "WST",
167 | "XAF",
168 | "XAG",
169 | "XAU",
170 | "XCD",
171 | "XDR",
172 | "XOF",
173 | "XPF",
174 | "YER",
175 | "ZAR",
176 | "ZMK",
177 | "ZMW",
178 | "ZWL"
179 | )
180 | val ALL = hashMapOf(
181 | "AED" to "United Arab Emirates Dirham",
182 | "AFN" to "Afghan Afghani",
183 | "ALL" to "Albanian Lek",
184 | "AMD" to "Armenian Dram",
185 | "ANG" to "Netherlands Antillean Guilder",
186 | "AOA" to "Angolan Kwanza",
187 | "ARS" to "Argentine Peso",
188 | "AUD" to "Australian Dollar",
189 | "AWG" to "Aruban Florin",
190 | "AZN" to "Azerbaijani Manat",
191 | "BAM" to "Bosnia-Herzegovina Convertible Mark",
192 | "BBD" to "Barbadian Dollar",
193 | "BDT" to "Bangladeshi Taka",
194 | "BGN" to "Bulgarian Lev",
195 | "BHD" to "Bahraini Dinar",
196 | "BIF" to "Burundian Franc",
197 | "BMD" to "Bermudan Dollar",
198 | "BND" to "Brunei Dollar",
199 | "BOB" to "Bolivian Boliviano",
200 | "BRL" to "Brazilian Real",
201 | "BSD" to "Bahamian Dollar",
202 | "BTC" to "Bitcoin",
203 | "BTN" to "Bhutanese Ngultrum",
204 | "BWP" to "Botswanan Pula",
205 | "BYN" to "New Belarusian Ruble",
206 | "BYR" to "Belarusian Ruble",
207 | "BZD" to "Belize Dollar",
208 | "CAD" to "Canadian Dollar",
209 | "CDF" to "Congolese Franc",
210 | "CHF" to "Swiss Franc",
211 | "CLF" to "Chilean Unit of Account (UF",
212 | "CLP" to "Chilean Peso",
213 | "CNY" to "Chinese Yuan",
214 | "COP" to "Colombian Peso",
215 | "CRC" to "Costa Rican Colón",
216 | "CUC" to "Cuban Convertible Peso",
217 | "CUP" to "Cuban Peso",
218 | "CVE" to "Cape Verdean Escudo",
219 | "CZK" to "Czech Republic Koruna",
220 | "DJF" to "Djiboutian Franc",
221 | "DKK" to "Danish Krone",
222 | "DOP" to "Dominican Peso",
223 | "DZD" to "Algerian Dinar",
224 | "EGP" to "Egyptian Pound",
225 | "ERN" to "Eritrean Nakfa",
226 | "ETB" to "Ethiopian Birr",
227 | "EUR" to "Euro",
228 | "FJD" to "Fijian Dollar",
229 | "FKP" to "Falkland Islands Pound",
230 | "GBP" to "British Pound Sterling",
231 | "GEL" to "Georgian Lari",
232 | "GGP" to "Guernsey Pound",
233 | "GHS" to "Ghanaian Cedi",
234 | "GIP" to "Gibraltar Pound",
235 | "GMD" to "Gambian Dalasi",
236 | "GNF" to "Guinean Franc",
237 | "GTQ" to "Guatemalan Quetzal",
238 | "GYD" to "Guyanaese Dollar",
239 | "HKD" to "Hong Kong Dollar",
240 | "HNL" to "Honduran Lempira",
241 | "HRK" to "Croatian Kuna",
242 | "HTG" to "Haitian Gourde",
243 | "HUF" to "Hungarian Forint",
244 | "IDR" to "Indonesian Rupiah",
245 | "ILS" to "Israeli New Sheqel",
246 | "IMP" to "Manx pound",
247 | "INR" to "Indian Rupee",
248 | "IQD" to "Iraqi Dinar",
249 | "IRR" to "Iranian Rial",
250 | "ISK" to "Icelandic Króna",
251 | "JEP" to "Jersey Pound",
252 | "JMD" to "Jamaican Dollar",
253 | "JOD" to "Jordanian Dinar",
254 | "JPY" to "Japanese Yen",
255 | "KES" to "Kenyan Shilling",
256 | "KGS" to "Kyrgystani Som",
257 | "KHR" to "Cambodian Riel",
258 | "KMF" to "Comorian Franc",
259 | "KPW" to "North Korean Won",
260 | "KRW" to "South Korean Won",
261 | "KWD" to "Kuwaiti Dinar",
262 | "KYD" to "Cayman Islands Dollar",
263 | "KZT" to "Kazakhstani Tenge",
264 | "LAK" to "Laotian Kip",
265 | "LBP" to "Lebanese Pound",
266 | "LKR" to "Sri Lankan Rupee",
267 | "LRD" to "Liberian Dollar",
268 | "LSL" to "Lesotho Loti",
269 | "LTL" to "Lithuanian Litas",
270 | "LVL" to "Latvian Lats",
271 | "LYD" to "Libyan Dinar",
272 | "MAD" to "Moroccan Dirham",
273 | "MDL" to "Moldovan Leu",
274 | "MGA" to "Malagasy Ariary",
275 | "MKD" to "Macedonian Denar",
276 | "MMK" to "Myanma Kyat",
277 | "MNT" to "Mongolian Tugrik",
278 | "MOP" to "Macanese Pataca",
279 | "MRO" to "Mauritanian Ouguiya",
280 | "MUR" to "Mauritian Rupee",
281 | "MVR" to "Maldivian Rufiyaa",
282 | "MWK" to "Malawian Kwacha",
283 | "MXN" to "Mexican Peso",
284 | "MYR" to "Malaysian Ringgit",
285 | "MZN" to "Mozambican Metical",
286 | "NAD" to "Namibian Dollar",
287 | "NGN" to "Nigerian Naira",
288 | "NIO" to "Nicaraguan Córdoba",
289 | "NOK" to "Norwegian Krone",
290 | "NPR" to "Nepalese Rupee",
291 | "NZD" to "New Zealand Dollar",
292 | "OMR" to "Omani Rial",
293 | "PAB" to "Panamanian Balboa",
294 | "PEN" to "Peruvian Nuevo Sol",
295 | "PGK" to "Papua New Guinean Kina",
296 | "PHP" to "Philippine Peso",
297 | "PKR" to "Pakistani Rupee",
298 | "PLN" to "Polish Zloty",
299 | "PYG" to "Paraguayan Guarani",
300 | "QAR" to "Qatari Rial",
301 | "RON" to "Romanian Leu",
302 | "RSD" to "Serbian Dinar",
303 | "RUB" to "Russian Ruble",
304 | "RWF" to "Rwandan Franc",
305 | "SAR" to "Saudi Riyal",
306 | "SBD" to "Solomon Islands Dollar",
307 | "SCR" to "Seychellois Rupee",
308 | "SDG" to "Sudanese Pound",
309 | "SEK" to "Swedish Krona",
310 | "SGD" to "Singapore Dollar",
311 | "SHP" to "Saint Helena Pound",
312 | "SLL" to "Sierra Leonean Leone",
313 | "SOS" to "Somali Shilling",
314 | "SRD" to "Surinamese Dollar",
315 | "STD" to "São Tomé and Príncipe Dobra",
316 | "SVC" to "Salvadoran Colón",
317 | "SYP" to "Syrian Pound",
318 | "SZL" to "Swazi Lilangeni",
319 | "THB" to "Thai Baht",
320 | "TJS" to "Tajikistani Somoni",
321 | "TMT" to "Turkmenistani Manat",
322 | "TND" to "Tunisian Dinar",
323 | "TOP" to "Tongan Paʻanga",
324 | "TRY" to "Turkish Lira",
325 | "TTD" to "Trinidad and Tobago Dollar",
326 | "TWD" to "New Taiwan Dollar",
327 | "TZS" to "Tanzanian Shilling",
328 | "UAH" to "Ukrainian Hryvnia",
329 | "UGX" to "Ugandan Shilling",
330 | "USD" to "United States Dollar",
331 | "UYU" to "Uruguayan Peso",
332 | "UZS" to "Uzbekistan Som",
333 | "VEF" to "Venezuelan Bolívar Fuerte",
334 | "VND" to "Vietnamese Dong",
335 | "VUV" to "Vanuatu Vatu",
336 | "WST" to "Samoan Tala",
337 | "XAF" to "CFA Franc BEAC",
338 | "XAG" to "Silver (troy ounce",
339 | "XAU" to "Gold (troy ounce",
340 | "XCD" to "East Caribbean Dollar",
341 | "XDR" to "Special Drawing Rights",
342 | "XOF" to "CFA Franc BCEAO",
343 | "XPF" to "CFP Franc",
344 | "YER" to "Yemeni Rial",
345 | "ZAR" to "South African Rand",
346 | "ZMK" to "Zambian Kwacha (pre-2013",
347 | "ZMW" to "Zambian Kwacha",
348 | "ZWL" to "Zimbabwean Dollar"
349 | )
350 | }
351 | }
352 |
--------------------------------------------------------------------------------