From 0be2c8b0b35c6869194d9f7912b020d6b8a0a7d6 Mon Sep 17 00:00:00 2001 From: Aneonex Software <69394046+aneonex@users.noreply.github.com> Date: Tue, 15 Nov 2022 01:07:01 +0300 Subject: [PATCH] Migrate test app to Jetpack Compose (#222) * Test app fully refactored and migrated to Jetpack Compose * Turn off dataBinding (not used) * Fixed log view * Fixed button state for market update pairs * Fixed currency in ticker view * Updated CapeCrypto exchange --- build.gradle | 13 + dataModule/build.gradle | 6 +- .../datamodule/model/market/CapeCrypto.kt | 90 +++-- dataModuleTester/build.gradle | 59 ++- .../1.json | 115 ++++++ dataModuleTester/src/main/AndroidManifest.xml | 25 +- .../bitcoinchecker/tester/MainActivity.kt | 359 ------------------ .../bitcoinchecker/tester/MyApplication.kt | 28 ++ .../bitcoinchecker/tester/data/HttpLogger.kt | 7 + .../tester/data/MyMarketRepository.kt | 18 + .../tester/data/MyMarketRepositoryImpl.kt | 97 +++++ .../bitcoinchecker/tester/data/TickerImpl.kt | 19 + .../tester/data/local/MarketDao.kt | 69 ++++ .../tester/data/local/MarketDatabase.kt | 25 ++ .../data/local/MyMarketLocalDataSource.kt | 35 ++ .../tester/data/local/model/MarketEntity.kt | 21 + .../data/local/model/MarketPairEntity.kt | 33 ++ .../data/local/model/MarketWithPairs.kt | 14 + .../tester/data/remote/HttpLoggerImpl.kt | 17 + .../data/remote/MyMarketRemoteDataSource.kt | 204 ++++++++++ .../tester/data/remote/util/Calls.kt | 41 ++ .../tester/di/LocalDataModule.kt | 24 ++ .../bitcoinchecker/tester/di/MarketModule.kt | 30 ++ .../tester/di/RemoteDataModule.kt | 36 ++ .../dialog/DynamicCurrencyPairsDialog.kt | 96 ----- .../tester/domain/exceptions/DatabaseError.kt | 3 + .../domain/exceptions/HttpMarketError.kt | 8 + .../tester/domain/exceptions/MarketError.kt | 49 +++ .../tester/domain/exceptions/NetworkError.kt | 5 + .../tester/domain/exceptions/ParseError.kt | 3 + .../tester/domain/exceptions/TimeoutError.kt | 3 + .../domain/exceptions/UnknownMarketError.kt | 3 + .../exceptions/UserFriendlyMarketError.kt | 3 + .../tester/domain/model/MarketTickerResult.kt | 10 + .../tester/domain/model/MyMarket.kt | 6 + .../tester/domain/model/MyMarketPairsInfo.kt | 42 ++ .../bitcoinchecker/tester/ui/MainActivity.kt | 20 + .../tester/ui/components/ComboBox.kt | 172 +++++++++ .../tester/ui/components/LogBox.kt | 46 +++ .../tester/ui/components/Scrollbar.kt | 51 +++ .../tester/ui/components/Ticker.kt | 104 +++++ .../tester/ui/features/error/ErrorScreen.kt | 97 +++++ .../ui/features/loading/LoadingScreen.kt | 38 ++ .../features/markettest/MarketTestScreen.kt | 328 ++++++++++++++++ .../markettest/MarketTestScreenViewState.kt | 31 ++ .../markettest/MarketTestViewModel.kt | 277 ++++++++++++++ .../markettest/dto/MarketPairsUpdateState.kt | 3 + .../ui/features/syncpairs/SyncPairsDialog.kt | 78 ++++ .../tester/ui/navigation/MyAppNavHost.kt | 32 ++ .../tester/ui/navigation/ScreenRoute.kt | 6 + .../bitcoinchecker/tester/ui/theme/Color.kt | 20 + .../bitcoinchecker/tester/ui/theme/Shape.kt | 11 + .../bitcoinchecker/tester/ui/theme/Theme.kt | 58 +++ .../tester/ui/theme/Typography.kt | 25 ++ .../tester/util/CheckErrorsUtils.kt | 75 ---- .../bitcoinchecker/tester/util/HttpsHelper.kt | 45 --- .../tester/util/MarketCurrencyPairsStore.kt | 39 -- .../tester/util/SpannableUtils.kt | 10 - .../tester/volley/CheckerErrorParsedError.kt | 10 - .../tester/volley/CheckerVolleyMainRequest.kt | 67 ---- .../tester/volley/CheckerVolleyNextRequest.kt | 14 - .../DynamicCurrencyPairsVolleyMainRequest.kt | 68 ---- .../DynamicCurrencyPairsVolleyNextRequest.kt | 12 - .../tester/volley/TickerImpl.kt | 15 - .../tester/volley/UnknownVolleyError.kt | 9 - .../generic/GenericCheckerVolleyRequest.kt | 8 - .../volley/generic/GzipVolleyRequest.kt | 156 -------- .../volley/generic/ResponseErrorListener.kt | 12 - .../tester/volley/generic/ResponseListener.kt | 11 - .../main/res/drawable-hdpi/ic_action_info.png | Bin 877 -> 0 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 3134 -> 0 bytes .../main/res/drawable-mdpi/ic_action_info.png | Bin 514 -> 0 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 2156 -> 0 bytes .../res/drawable-xhdpi/ic_action_info.png | Bin 1129 -> 0 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 3964 -> 0 bytes .../res/drawable-xxhdpi/ic_action_info.png | Bin 1889 -> 0 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 5807 -> 0 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 7675 -> 0 bytes .../src/main/res/drawable/ic_error.xml | 134 +++++++ .../res/drawable/ic_launcher_foreground.xml | 20 + .../main/res/drawable/ic_no_connection.xml | 160 ++++++++ .../layout/dynamic_currency_pairs_dialog.xml | 32 -- .../src/main/res/layout/main_activity.xml | 155 -------- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../src/main/res/values/check_errors.xml | 12 - .../src/main/res/values/colors.xml | 25 ++ .../res/values/ic_launcher_background.xml | 4 + .../src/main/res/values/strings.xml | 49 ++- .../src/main/res/values/styles.xml | 7 - .../src/main/res/values/themes.xml | 10 + .../src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + gradle.properties | 2 +- 93 files changed, 2927 insertions(+), 1284 deletions(-) create mode 100644 dataModuleTester/schemas/com.aneonex.bitcoinchecker.tester.data.local.MarketDatabase/1.json delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/MainActivity.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/MyApplication.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/HttpLogger.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/MyMarketRepository.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/MyMarketRepositoryImpl.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/TickerImpl.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MarketDao.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MarketDatabase.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MyMarketLocalDataSource.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketEntity.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketPairEntity.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketWithPairs.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/HttpLoggerImpl.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/MyMarketRemoteDataSource.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/util/Calls.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/LocalDataModule.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/MarketModule.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/RemoteDataModule.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/dialog/DynamicCurrencyPairsDialog.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/DatabaseError.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/HttpMarketError.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/MarketError.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/NetworkError.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/ParseError.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/TimeoutError.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/UnknownMarketError.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/UserFriendlyMarketError.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MarketTickerResult.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MyMarket.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MyMarketPairsInfo.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/MainActivity.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/ComboBox.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/LogBox.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/Scrollbar.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/Ticker.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/error/ErrorScreen.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/loading/LoadingScreen.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestScreen.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestScreenViewState.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestViewModel.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/dto/MarketPairsUpdateState.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/syncpairs/SyncPairsDialog.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/navigation/MyAppNavHost.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/navigation/ScreenRoute.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Color.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Shape.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Theme.kt create mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Typography.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/CheckErrorsUtils.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/HttpsHelper.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/MarketCurrencyPairsStore.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/SpannableUtils.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerErrorParsedError.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerVolleyMainRequest.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerVolleyNextRequest.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/DynamicCurrencyPairsVolleyMainRequest.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/DynamicCurrencyPairsVolleyNextRequest.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/TickerImpl.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/UnknownVolleyError.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/GenericCheckerVolleyRequest.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/GzipVolleyRequest.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/ResponseErrorListener.kt delete mode 100644 dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/ResponseListener.kt delete mode 100644 dataModuleTester/src/main/res/drawable-hdpi/ic_action_info.png delete mode 100644 dataModuleTester/src/main/res/drawable-hdpi/ic_launcher.png delete mode 100644 dataModuleTester/src/main/res/drawable-mdpi/ic_action_info.png delete mode 100644 dataModuleTester/src/main/res/drawable-mdpi/ic_launcher.png delete mode 100644 dataModuleTester/src/main/res/drawable-xhdpi/ic_action_info.png delete mode 100644 dataModuleTester/src/main/res/drawable-xhdpi/ic_launcher.png delete mode 100644 dataModuleTester/src/main/res/drawable-xxhdpi/ic_action_info.png delete mode 100644 dataModuleTester/src/main/res/drawable-xxhdpi/ic_launcher.png delete mode 100644 dataModuleTester/src/main/res/drawable-xxxhdpi/ic_launcher.png create mode 100644 dataModuleTester/src/main/res/drawable/ic_error.xml create mode 100644 dataModuleTester/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 dataModuleTester/src/main/res/drawable/ic_no_connection.xml delete mode 100644 dataModuleTester/src/main/res/layout/dynamic_currency_pairs_dialog.xml delete mode 100644 dataModuleTester/src/main/res/layout/main_activity.xml create mode 100644 dataModuleTester/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 dataModuleTester/src/main/res/values/check_errors.xml create mode 100644 dataModuleTester/src/main/res/values/colors.xml create mode 100644 dataModuleTester/src/main/res/values/ic_launcher_background.xml delete mode 100644 dataModuleTester/src/main/res/values/styles.xml create mode 100644 dataModuleTester/src/main/res/values/themes.xml create mode 100644 dataModuleTester/src/main/res/xml/backup_rules.xml create mode 100644 dataModuleTester/src/main/res/xml/data_extraction_rules.xml diff --git a/build.gradle b/build.gradle index 788f0f10..e7490fac 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,15 @@ buildscript { kotlin_version = '1.7.20' core_ktx_version = '1.9.0' + coroutine_version = '1.6.4' + + compose_bom_version = '2022.11.00' + compose_compiler_version = '1.3.2' + + hilt_version = '2.44.1' + roomVersion = '2.4.3' + + okhttp_version = '5.0.0-alpha.10' } repositories { @@ -17,6 +26,9 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + // DI support + classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } } @@ -24,6 +36,7 @@ allprojects { repositories { google() mavenCentral() +// maven { url 'https://jitpack.io' } } } diff --git a/dataModule/build.gradle b/dataModule/build.gradle index 78825c1e..dd54ed65 100644 --- a/dataModule/build.gradle +++ b/dataModule/build.gradle @@ -29,10 +29,10 @@ android { kotlinOptions { jvmTarget = '11' } - - lintOptions { + lint { abortOnError true - checkReleaseBuilds true checkDependencies true + checkReleaseBuilds true } + } \ No newline at end of file diff --git a/dataModule/src/main/java/com/aneonex/bitcoinchecker/datamodule/model/market/CapeCrypto.kt b/dataModule/src/main/java/com/aneonex/bitcoinchecker/datamodule/model/market/CapeCrypto.kt index 187c2d69..7f2950ac 100644 --- a/dataModule/src/main/java/com/aneonex/bitcoinchecker/datamodule/model/market/CapeCrypto.kt +++ b/dataModule/src/main/java/com/aneonex/bitcoinchecker/datamodule/model/market/CapeCrypto.kt @@ -1,29 +1,42 @@ package com.aneonex.bitcoinchecker.datamodule.model.market import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo +import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo import com.aneonex.bitcoinchecker.datamodule.model.Market import com.aneonex.bitcoinchecker.datamodule.model.Ticker -import com.aneonex.bitcoinchecker.datamodule.model.currency.Currency -import com.aneonex.bitcoinchecker.datamodule.model.currency.CurrencyPairsMap -import com.aneonex.bitcoinchecker.datamodule.model.currency.VirtualCurrency +import com.aneonex.bitcoinchecker.datamodule.util.forEachJSONObject import org.json.JSONArray import org.json.JSONObject -class CapeCrypto : Market(NAME, TTS_NAME, CURRENCY_PAIRS) { +class CapeCrypto : Market(NAME, TTS_NAME) { companion object { private const val NAME = "Cape Crypto" private const val TTS_NAME = NAME - // private const val URL_TICKER = "https://trade.capecrypto.com/api/v2/peatio/public/markets/btczar/tickers" - private const val URL_TICKER = "https://trade.capecrypto.com/api/v2/peatio/public/markets/%1\$s%2\$s/tickers" - private const val URL_ORDERS = "https://trade.capecrypto.com/api/v2/peatio/public/markets/%1\$s%2\$s/order-book?asks_limit=1&bids_limit=1" - private val CURRENCY_PAIRS: CurrencyPairsMap = CurrencyPairsMap() + private const val URL_PAIRS = "https://trade.capecrypto.com/api/v2/peatio/public/markets" + private const val URL_TICKER = "https://trade.capecrypto.com/api/v2/peatio/public/markets/%1\$s/tickers" + private const val URL_ORDERS = "https://trade.capecrypto.com/api/v2/peatio/public/markets/%1\$s/order-book?asks_limit=1&bids_limit=1" + } - init { - CURRENCY_PAIRS[VirtualCurrency.BTC] = arrayOf( - Currency.ZAR - ) - } + override fun getCurrencyPairsUrl(requestId: Int): String { + return URL_PAIRS + } + + override fun parseCurrencyPairs( + requestId: Int, + responseString: String, + pairs: MutableList + ) { + JSONArray(responseString) + .forEachJSONObject { market -> + pairs.add( + CurrencyPairInfo( + market.getString("base_unit").uppercase(), + market.getString("quote_unit").uppercase(), + market.getString("id") + ) + ) + } } override fun getNumOfRequests(checkerInfo: CheckerInfo?): Int { @@ -31,36 +44,53 @@ class CapeCrypto : Market(NAME, TTS_NAME, CURRENCY_PAIRS) { } override fun getUrl(requestId: Int, checkerInfo: CheckerInfo): String { + val pairId = checkerInfo.currencyPairId ?: with(checkerInfo){"$currencyBaseLowerCase$currencyCounterLowerCase"} return if (requestId == 0) { - String.format(URL_TICKER, checkerInfo.currencyBaseLowerCase, checkerInfo.currencyCounterLowerCase) + String.format(URL_TICKER, pairId) } else { - String.format(URL_ORDERS, checkerInfo.currencyBaseLowerCase, checkerInfo.currencyCounterLowerCase) + String.format(URL_ORDERS, pairId) } } @Throws(Exception::class) - override fun parseTickerFromJsonObject(requestId: Int, jsonObject: JSONObject, ticker: Ticker, - checkerInfo: CheckerInfo) { + override fun parseTickerFromJsonObject( + requestId: Int, + jsonObject: JSONObject, + ticker: Ticker, + checkerInfo: CheckerInfo + ) { if (requestId == 0) { - ticker.vol = jsonObject.getJSONObject("ticker").getDouble("vol") - ticker.high = jsonObject.getJSONObject("ticker").getDouble("high") - ticker.low = jsonObject.getJSONObject("ticker").getDouble("low") - ticker.last = jsonObject.getJSONObject("ticker").getDouble("last") ticker.timestamp = jsonObject.getLong("at") + + jsonObject.getJSONObject("ticker").also { + ticker.last = it.getDouble("last") + + ticker.vol = it.getDouble("amount") + ticker.volQuote = it.getDouble("vol") + + ticker.high = it.getDouble("high") + ticker.low = it.getDouble("low") + } } else { - val jArrayBids: JSONArray = jsonObject.getJSONArray("bids") - val jArrayAsks: JSONArray = jsonObject.getJSONArray("asks") - val jResultBids = jArrayBids.getJSONObject(0) - val jResultAsks = jArrayAsks.getJSONObject(0) - ticker.bid = jResultBids.getDouble("price") - ticker.ask = jResultAsks.getDouble("price") + jsonObject + .getJSONArray("bids") + .getJSONObject(0) + .also { ticker.bid = it.getDouble("price") } + + jsonObject + .getJSONArray("asks") + .getJSONObject(0) + .also { ticker.ask = it.getDouble("price") } } } @Throws(Exception::class) - override fun parseErrorFromJsonObject(requestId: Int, jsonObject: JSONObject, - checkerInfo: CheckerInfo?): String? { - return jsonObject.getString("message") + override fun parseErrorFromJsonObject( + requestId: Int, + jsonObject: JSONObject, + checkerInfo: CheckerInfo? + ): String? { + return jsonObject.getJSONArray("errors").getString(0) } } diff --git a/dataModuleTester/build.gradle b/dataModuleTester/build.gradle index 4a08e740..c0c01749 100644 --- a/dataModuleTester/build.gradle +++ b/dataModuleTester/build.gradle @@ -1,6 +1,8 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' } android { @@ -10,7 +12,7 @@ android { buildToolsVersion build_tools_version buildFeatures { - viewBinding true + compose = true } defaultConfig { @@ -20,6 +22,12 @@ android { versionCode 1 versionName "1.0" + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } buildTypes { @@ -34,24 +42,59 @@ android { targetCompatibility JavaVersion.VERSION_11 } + composeOptions { + kotlinCompilerExtensionVersion = "$compose_compiler_version" + } + kotlinOptions { jvmTarget = '11' } - lintOptions { + lint { abortOnError true disable 'MissingTranslation' + enable 'LogNotTimber' } } dependencies { implementation project(':dataModule') - implementation 'com.google.code.gson:gson:2.9.0' - implementation 'com.android.volley:volley:1.2.1' - // AndroidX implementation "androidx.core:core-ktx:$core_ktx_version" - implementation "androidx.appcompat:appcompat:1.5.1" - implementation 'com.google.android.material:material:1.7.0' -} \ No newline at end of file + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version") + + // DI: Hilt dependencies + implementation "com.google.dagger:hilt-android:$hilt_version" + kapt "com.google.dagger:hilt-android-compiler:$hilt_version" + + // Jetpack Compose + def composeBom = platform("androidx.compose:compose-bom:$compose_bom_version") + implementation composeBom + androidTestImplementation composeBom + + implementation "androidx.compose.material3:material3" + + // Compose Android Studio preview support + implementation "androidx.compose.ui:ui-tooling-preview" + implementation "androidx.compose.ui:ui-util" + debugImplementation "androidx.compose.ui:ui-tooling" + + // Compose dependencies + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1" + implementation "androidx.navigation:navigation-compose:2.5.3" +// implementation "androidx.compose.material:material-icons-extended:$compose_version" + implementation "androidx.hilt:hilt-navigation-compose:1.0.0" + + implementation "androidx.room:room-ktx:$roomVersion" + kapt "androidx.room:room-compiler:$roomVersion" + + implementation("com.squareup.okhttp3:okhttp:$okhttp_version") + implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_version") + + implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") + + // Logging + implementation 'com.jakewharton.timber:timber:5.0.1' +} diff --git a/dataModuleTester/schemas/com.aneonex.bitcoinchecker.tester.data.local.MarketDatabase/1.json b/dataModuleTester/schemas/com.aneonex.bitcoinchecker.tester.data.local.MarketDatabase/1.json new file mode 100644 index 00000000..73db3c37 --- /dev/null +++ b/dataModuleTester/schemas/com.aneonex.bitcoinchecker.tester.data.local.MarketDatabase/1.json @@ -0,0 +1,115 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "3114016120a0732454bcf8c5b9ed35d2", + "entities": [ + { + "tableName": "markets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `marketKey` TEXT NOT NULL COLLATE NOCASE, `updateDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "marketKey", + "columnName": "marketKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updateDate", + "columnName": "updateDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_markets_marketKey", + "unique": false, + "columnNames": [ + "marketKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_markets_marketKey` ON `${TABLE_NAME}` (`marketKey`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "market_pairs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`marketId` INTEGER NOT NULL, `baseAsset` TEXT NOT NULL, `quoteAsset` TEXT NOT NULL, `contractType` TEXT NOT NULL, `marketPairId` TEXT, PRIMARY KEY(`marketId`, `baseAsset`, `quoteAsset`, `contractType`), FOREIGN KEY(`marketId`) REFERENCES `markets`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "marketId", + "columnName": "marketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseAsset", + "columnName": "baseAsset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "quoteAsset", + "columnName": "quoteAsset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contractType", + "columnName": "contractType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marketPairId", + "columnName": "marketPairId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "marketId", + "baseAsset", + "quoteAsset", + "contractType" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "markets", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "marketId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3114016120a0732454bcf8c5b9ed35d2')" + ] + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/AndroidManifest.xml b/dataModuleTester/src/main/AndroidManifest.xml index 80dfee6c..2a64ec02 100644 --- a/dataModuleTester/src/main/AndroidManifest.xml +++ b/dataModuleTester/src/main/AndroidManifest.xml @@ -1,22 +1,29 @@ - + + - - + + - + - + \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/MainActivity.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/MainActivity.kt deleted file mode 100644 index 837d920c..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/MainActivity.kt +++ /dev/null @@ -1,359 +0,0 @@ -package com.aneonex.bitcoinchecker.tester - -import android.content.Context -import android.os.Bundle -import android.text.SpannableStringBuilder -import android.view.View -import android.widget.AdapterView -import android.widget.AdapterView.OnItemSelectedListener -import android.widget.ArrayAdapter -import android.widget.SpinnerAdapter -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible -import com.android.volley.NetworkResponse -import com.android.volley.RequestQueue -import com.android.volley.VolleyError -import com.aneonex.bitcoinchecker.datamodule.config.MarketsConfig -import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo -import com.aneonex.bitcoinchecker.datamodule.model.FuturesContractType -import com.aneonex.bitcoinchecker.datamodule.model.Market -import com.aneonex.bitcoinchecker.datamodule.model.Ticker -import com.aneonex.bitcoinchecker.datamodule.util.CurrencyPairsMapHelper -import com.aneonex.bitcoinchecker.datamodule.util.FormatUtilsBase -import com.aneonex.bitcoinchecker.datamodule.util.MarketsConfigUtils.getMarketByKey -import com.aneonex.bitcoinchecker.tester.databinding.MainActivityBinding -import com.aneonex.bitcoinchecker.tester.dialog.DynamicCurrencyPairsDialog -import com.aneonex.bitcoinchecker.tester.util.CheckErrorsUtils -import com.aneonex.bitcoinchecker.tester.util.HttpsHelper -import com.aneonex.bitcoinchecker.tester.util.MarketCurrencyPairsStore -import com.aneonex.bitcoinchecker.tester.volley.CheckerErrorParsedError -import com.aneonex.bitcoinchecker.tester.volley.CheckerVolleyMainRequest -import com.aneonex.bitcoinchecker.tester.volley.CheckerVolleyMainRequest.TickerWrapper -import com.aneonex.bitcoinchecker.tester.volley.generic.ResponseErrorListener -import com.aneonex.bitcoinchecker.tester.volley.generic.ResponseListener - -class MainActivity : AppCompatActivity() { - private lateinit var binding: MainActivityBinding - - private inner class MarketEntry(var key: String, var name: String) { - override fun toString(): String { - return name - } - } - - private lateinit var requestQueue: RequestQueue - private lateinit var currencyPairsMapHelper: CurrencyPairsMapHelper - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = MainActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - - requestQueue = HttpsHelper.newRequestQueue(this) - - refreshMarketSpinner() - currencyPairsMapHelper = createCurrencyPairsMapHelper(this@MainActivity, selectedMarket) - refreshCurrencySpinners() - showResultView(true) - binding.marketSpinner.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected(arg0: AdapterView<*>?, arg1: View?, arg2: Int, arg3: Long) { - currencyPairsMapHelper = createCurrencyPairsMapHelper(this@MainActivity, selectedMarket) - binding.resultView.text = "" - refreshCurrencySpinners() - } - - override fun onNothingSelected(arg0: AdapterView<*>?) { - // do nothing - } - } - binding.dynamicCurrencyPairsInfoView.setOnClickListener { - object : DynamicCurrencyPairsDialog(this@MainActivity, selectedMarket, currencyPairsMapHelper) { - override fun onPairsUpdated(market: Market, currencyPairsMapHelper: CurrencyPairsMapHelper?) { - this@MainActivity.currencyPairsMapHelper = currencyPairsMapHelper ?: createCurrencyPairsMapHelper(this@MainActivity, selectedMarket) - refreshCurrencySpinners() - } - }.show() - } - binding.currencyBaseSpinner.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected(arg0: AdapterView<*>?, arg1: View?, arg2: Int, arg3: Long) { - refreshCurrencyCounterSpinner() - } - - override fun onNothingSelected(arg0: AdapterView<*>?) { - // do nothing - } - } - binding.currencyCounterSpinner.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected(arg0: AdapterView<*>?, arg1: View?, arg2: Int, arg3: Long) { - refreshFuturesContractTypeSpinner() - } - - override fun onNothingSelected(arg0: AdapterView<*>?) { - // do nothing - } - } - - binding.getResultButton.setOnClickListener { newResult } - -// binding.testAllButton.setOnClickListener { testAllExchanges() } -// binding.testAllButton.isVisible = true - } - - // ==================== - // Get selected items - // ==================== - private val selectedMarket: Market - get() { - val marketEntry = binding.marketSpinner.selectedItem as MarketEntry - return getMarketByKey(marketEntry.key) - } - - private val selectedCurrencyBase: String? - get() = if (binding.currencyBaseSpinner.adapter == null) null else binding.currencyBaseSpinner.selectedItem.toString() - private val selectedCurrencyCounter: String? - get() = if (binding.currencyCounterSpinner.adapter == null) null else binding.currencyCounterSpinner.selectedItem.toString() - private val selectedFuturesContractType: FuturesContractType - get() = if (binding.currencyCounterSpinner.adapter == null || !binding.futuresContractTypeSpinner.isVisible) FuturesContractType.NONE - else binding.futuresContractTypeSpinner.selectedItem as FuturesContractType - - // ==================== - // Refreshing UI - // ==================== - private fun refreshMarketSpinner() { -/* - val entries = arrayOfNulls(MarketsConfig.MARKETS.size) - var i = entries.size - 1 - for (market in MarketsConfig.MARKETS.values) { - val marketEntry = MarketEntry(market.key, market.name) - entries[i--] = marketEntry // market.name; - } -*/ - val entries = MarketsConfig.MARKETS.values - .sortedBy { market -> market.name.uppercase() } - .map { market -> MarketEntry(market.key, market.name) } - - binding.marketSpinner.adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, entries) - } - - private fun refreshCurrencySpinners() { - refreshCurrencyBaseSpinner() - refreshCurrencyCounterSpinner() - refreshDynamicCurrencyPairsView() - val isCurrencyEmpty = selectedCurrencyBase == null || selectedCurrencyCounter == null - binding.currencySpinnersWrapper.isVisible = !isCurrencyEmpty - binding.dynamicCurrencyPairsWarningView.isVisible = isCurrencyEmpty - binding.getResultButton.isVisible = !isCurrencyEmpty - } - - private fun refreshDynamicCurrencyPairsView() { - binding.dynamicCurrencyPairsInfoView.isEnabled = selectedMarket.getCurrencyPairsUrl(0) != null - } - - private fun refreshCurrencyBaseSpinner() { - if (!currencyPairsMapHelper.isEmpty()) { - binding.currencyBaseSpinner.adapter = ArrayAdapter( - this, - android.R.layout.simple_spinner_dropdown_item, - currencyPairsMapHelper.baseAssets.toList() - ) - } else { - binding.currencyBaseSpinner.adapter = null - } - } - - private fun refreshCurrencyCounterSpinner() { - val baseAsset = selectedCurrencyBase - if (!currencyPairsMapHelper.isEmpty() && !baseAsset.isNullOrEmpty()) { - binding.currencyCounterSpinner.adapter = ArrayAdapter( - this, - android.R.layout.simple_spinner_dropdown_item, - currencyPairsMapHelper.getQuoteAssets(baseAsset).toList() - ) - } else { - binding.currencyCounterSpinner.adapter = null - } - - refreshFuturesContractTypeSpinner() - } - - private fun refreshFuturesContractTypeSpinner() { - val availableContractTypes = currencyPairsMapHelper.getAvailableFuturesContractsTypes(selectedCurrencyBase, selectedCurrencyCounter) - var spinnerAdapter: SpinnerAdapter? = null - if (availableContractTypes.any { it != FuturesContractType.NONE }) { -/* - val entries = arrayOfNulls(market.contractTypes.size) - for (i in market.contractTypes.indices) { - val contractType = market.contractTypes[i] - entries[i] = getContractTypeShortName(contractType) - } - */ - spinnerAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, availableContractTypes.toTypedArray()) - } - binding.futuresContractTypeSpinner.adapter = spinnerAdapter - binding.futuresContractTypeSpinner.isVisible = spinnerAdapter != null - } - - private fun showResultView(showResultView: Boolean) { - binding.getResultButton.isEnabled = showResultView - binding.progressBar.isVisible = !showResultView - binding.resultView.isVisible = showResultView - } -/* - private fun getProperCurrencyPairs(market: Market): HashMap>? { - val currencyPairsMapHelper = currencyPairsMapHelper - - return if (!currencyPairsMapHelper.isEmpty()) - currencyPairsMapHelper.pa - else market.currencyPairs - } -*/ - - // ==================== - // Get && display results - // ==================== - private val newResult: Unit - get() { - val market = selectedMarket - val currencyBase = selectedCurrencyBase - val currencyCounter = selectedCurrencyCounter - // val contractType = getSelectedContractType(market) - val contractType = selectedFuturesContractType - val pairId = currencyPairsMapHelper.getCurrencyPairId(currencyBase, currencyCounter, contractType) - val checkerInfo = CheckerInfo(currencyBase!!, currencyCounter!!, pairId, contractType) - val request = CheckerVolleyMainRequest(market, checkerInfo, - object : ResponseListener() { - override fun onResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, response: TickerWrapper?) { - handleNewResult(checkerInfo, response?.ticker, url, requestHeaders, networkResponse, responseString, null, null) - } - }, object : ResponseErrorListener() { - override fun onErrorResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, error: VolleyError) { - error.printStackTrace() - var errorMsg: String? = null - if (error is CheckerErrorParsedError) { - errorMsg = error.errorMsg - } - if (errorMsg.isNullOrEmpty()) errorMsg = CheckErrorsUtils.parseVolleyErrorMsg(this@MainActivity, error) - handleNewResult(checkerInfo, null, url, requestHeaders, networkResponse, responseString, errorMsg, error) - } - }) - requestQueue.add(request) - showResultView(false) - } - - private fun handleNewResult(checkerInfo: CheckerInfo, ticker: Ticker?, url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, rawResponse: String?, errorMsg: String?, error: VolleyError?) { - showResultView(true) - val ssb = SpannableStringBuilder() - if (ticker != null) { - ssb.append(getString(R.string.ticker_last, FormatUtilsBase.formatPriceWithCurrency(ticker.last, checkerInfo.currencyCounter))) - ssb.append(createNewPriceLineIfNeeded(R.string.ticker_high, ticker.high, checkerInfo.currencyCounter)) - ssb.append(createNewPriceLineIfNeeded(R.string.ticker_low, ticker.low, checkerInfo.currencyCounter)) - ssb.append(createNewPriceLineIfNeeded(R.string.ticker_bid, ticker.bid, checkerInfo.currencyCounter)) - ssb.append(createNewPriceLineIfNeeded(R.string.ticker_ask, ticker.ask, checkerInfo.currencyCounter)) - ssb.append(createNewPriceLineIfNeeded(R.string.ticker_vol_base, ticker.vol, checkerInfo.currencyBase)) - ssb.append(createNewPriceLineIfNeeded(R.string.ticker_vol_quote, ticker.volQuote, checkerInfo.currencyCounter)) - ssb.append(""" - - ${getString(R.string.ticker_timestamp, FormatUtilsBase.formatSameDayTimeOrDate(this, ticker.timestamp))} - """.trimIndent()) - } else { - ssb.append(getString(R.string.check_error_generic_prefix, errorMsg ?: "UNKNOWN")) - } - CheckErrorsUtils.formatResponseDebug(this, ssb, url, requestHeaders, networkResponse, rawResponse, error) - binding.resultView.text = ssb - } - - private fun createNewPriceLineIfNeeded(textResId: Int, price: Double, currency: String): String { - return if (price <= Ticker.NO_DATA) "" else """ - - ${getString(textResId, FormatUtilsBase.formatPriceWithCurrency(price, currency))} - """.trimIndent() - } - -/* - private fun testAllExchanges(){ - Toast.makeText(this, "Test all", Toast.LENGTH_SHORT).show() - - for (market in MarketsConfig.MARKETS.values) { - Log.d("TEST", "*** Checking: ${market.name} (${market.key}) ***") - - val marketPairs = createCurrencyPairsMapHelper(this, market) - - if(marketPairs.isEmpty()){ - Log.d("TEST", "No pairs, queue updating...") - requestQueue.add(createRequestDynamicCurrencyPairs(this, market)) - - continue - } - - val currencyBase = marketPairs.baseAssets.first() - val currencyCounter = marketPairs.getQuoteAssets(currencyBase).first() - - Log.d("TEST", "First pair: $currencyBase/$currencyCounter") - - val contractType = marketPairs - .getAvailableFuturesContractsTypes(currencyBase, currencyCounter) - .firstOrNull() ?: FuturesContractType.NONE - val pairId = marketPairs.getCurrencyPairId(currencyBase, currencyCounter, contractType) - - val checkerInfo = CheckerInfo(currencyBase, currencyCounter, pairId, contractType) - val request = CheckerVolleyMainRequest(market, checkerInfo, - object : ResponseListener() { - override fun onResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, response: TickerWrapper?) { - handleTestExchange(market.name, url, null) - } - }, object : ResponseErrorListener() { - override fun onErrorResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, error: VolleyError) { -// error.printStackTrace() - handleTestExchange(market.name, url, error) - } - }) - requestQueue.add(request) - } - } - - private fun handleTestExchange(marketName: String, url: String?, error: VolleyError?) { -// showResultView(true) - - val sb = StringBuilder() - - sb.append("TEST_RESULT [$marketName]: ") - - if(error == null) - sb.append("Success") - else - sb.append("FAILED") - - sb.append(": ") - sb.append(url ?: "Unknown uri") - - if(error?.cause != null) - sb.append("\nDetails: ${error.cause}") - - Log.d("TEST", sb.toString()) - } - - private fun createRequestDynamicCurrencyPairs(context: Context, market: Market): DynamicCurrencyPairsVolleyMainRequest { - return DynamicCurrencyPairsVolleyMainRequest(context, market, - object : ResponseListener() { - override fun onResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, response: CurrencyPairsMapHelper?) { - handleTestExchange(market.name, url, null) - } - }, object : ResponseErrorListener() { - override fun onErrorResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, error: VolleyError) { - handleTestExchange(market.name, url, error) - } - }) - } -*/ - - companion object { - private fun createCurrencyPairsMapHelper(context: Context, market: Market): CurrencyPairsMapHelper { - val pairsFromStore = MarketCurrencyPairsStore.getPairsForMarket(context, market.key) - return if(pairsFromStore != null) - CurrencyPairsMapHelper(pairsFromStore) - else - CurrencyPairsMapHelper(market.currencyPairs) - } - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/MyApplication.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/MyApplication.kt new file mode 100644 index 00000000..78cc74e1 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/MyApplication.kt @@ -0,0 +1,28 @@ +package com.aneonex.bitcoinchecker.tester + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber +import timber.log.Timber.DebugTree +import timber.log.Timber.Forest.plant + + +@HiltAndroidApp +class MyApplication : Application() { + + override fun onCreate() { + super.onCreate() + + initializeLogging() + + Timber.i("*** Application started ***") + } + + companion object { + private fun initializeLogging(){ + if (BuildConfig.DEBUG) { + plant(DebugTree()) + } + } + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/HttpLogger.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/HttpLogger.kt new file mode 100644 index 00000000..07f5517a --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/HttpLogger.kt @@ -0,0 +1,7 @@ +package com.aneonex.bitcoinchecker.tester.data + +import kotlinx.coroutines.flow.SharedFlow + +interface HttpLogger { + val messageFlow: SharedFlow +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/MyMarketRepository.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/MyMarketRepository.kt new file mode 100644 index 00000000..5f78b622 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/MyMarketRepository.kt @@ -0,0 +1,18 @@ +package com.aneonex.bitcoinchecker.tester.data + +import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo +import com.aneonex.bitcoinchecker.tester.domain.model.MarketTickerResult +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarket +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo + +interface MyMarketRepository { + suspend fun getMarketList(): List + suspend fun getMarketCurrencyPairsInfo(market: MyMarket): MyMarketPairsInfo + fun isMarketSupportsUpdatePairs(market: MyMarket): Boolean + +// suspend fun getBaseCurrencies(market: MyMarket): List +// suspend fun getQuoteCurrencies(market: MyMarket, baseCurrency: String): List + + suspend fun getMarketTicker(market: MyMarket, pairInfo: CurrencyPairInfo): MarketTickerResult + suspend fun updateMarketCurrencyPairs(market: MyMarket) +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/MyMarketRepositoryImpl.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/MyMarketRepositoryImpl.kt new file mode 100644 index 00000000..8b7fec1a --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/MyMarketRepositoryImpl.kt @@ -0,0 +1,97 @@ +package com.aneonex.bitcoinchecker.tester.data + +import android.content.res.Resources +import com.aneonex.bitcoinchecker.datamodule.config.MarketsConfig +import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo +import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo +import com.aneonex.bitcoinchecker.datamodule.model.Market +import com.aneonex.bitcoinchecker.datamodule.model.currency.CurrencyPairsMap +import com.aneonex.bitcoinchecker.tester.data.local.MyMarketLocalDataSource +import com.aneonex.bitcoinchecker.tester.data.remote.MyMarketRemoteDataSource +import com.aneonex.bitcoinchecker.tester.domain.exceptions.rethrowIfCritical +import com.aneonex.bitcoinchecker.tester.domain.model.MarketTickerResult +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarket +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo +import kotlinx.coroutines.delay +import javax.inject.Inject + +class MyMarketRepositoryImpl @Inject constructor( +// @IoDispatcher private val ioDispatcher: CoroutineDispatcher + private val marketLocalDataSource: MyMarketLocalDataSource, + private val marketRemoteDataSource: MyMarketRemoteDataSource + ) : MyMarketRepository { + + override suspend fun getMarketList(): List { + delay(3000) // Emulate loading + + return MarketsConfig.MARKETS.values.toList() + .map{ it.mapToMyMarket() } + } + + override fun isMarketSupportsUpdatePairs(market: MyMarket): Boolean = + !getSourceMarketByKey(market).getCurrencyPairsUrl(0).isNullOrEmpty() + + + override suspend fun getMarketCurrencyPairsInfo(market: MyMarket): MyMarketPairsInfo { + return marketLocalDataSource.getMarketData(market.key) ?: + MyMarketPairsInfo( + lastSyncDate = 0, + pairs = getSourceMarketByKey(market).currencyPairs?.let { convertPairsMapToPairList(it) } ?: emptyList() + ) + } + + override suspend fun getMarketTicker( + market: MyMarket, + pairInfo: CurrencyPairInfo + ): MarketTickerResult { + + val checkerInfo = pairInfo.toCheckerInfo() + + return try { + val ticker = marketRemoteDataSource.fetchMarketTicker( + getSourceMarketByKey(market), + checkerInfo + ) + MarketTickerResult(ticker, checkerInfo) + } catch (ex: Exception) { + ex.rethrowIfCritical() + + MarketTickerResult(TickerImpl(), checkerInfo, ex.message) // TODO: Parse exception + } + } + + override suspend fun updateMarketCurrencyPairs(market: MyMarket) { + val marketData = + marketRemoteDataSource.fetchMarketCurrencyPairsInfo(getSourceMarketByKey(market)) + + if(marketData.size > 0) { + marketLocalDataSource.saveMarketData(market.key, marketData) + } + } +} + +private fun CurrencyPairInfo.toCheckerInfo(): CheckerInfo = + CheckerInfo( + this.currencyBase, + this.currencyCounter, + this.currencyPairId, + this.contractType + ) + +private fun getSourceMarketByKey(market: MyMarket): Market = + getSourceMarketByKey(market.key) + +private fun getSourceMarketByKey(marketKey: String): Market = + MarketsConfig.MARKETS[marketKey] + ?: throw Resources.NotFoundException("Market not found (key=${marketKey})") + +private fun Market.mapToMyMarket(): MyMarket = + MyMarket(key = key, name = name) + +private fun convertPairsMapToPairList(currencyMap: CurrencyPairsMap): List { + return currencyMap.flatMap { item -> + item.value.map { quoteAsset -> + CurrencyPairInfo(item.key, quoteAsset, null) + } + } +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/TickerImpl.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/TickerImpl.kt new file mode 100644 index 00000000..4724f8ce --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/TickerImpl.kt @@ -0,0 +1,19 @@ +package com.aneonex.bitcoinchecker.tester.data + +import com.aneonex.bitcoinchecker.datamodule.model.Ticker +import com.aneonex.bitcoinchecker.datamodule.model.Ticker.Companion.NO_DATA + +internal class TickerImpl: Ticker { + override var bid: Double = NO_DATA_DOUBLE + override var ask: Double = NO_DATA_DOUBLE + override var vol: Double = NO_DATA_DOUBLE + override var volQuote: Double = NO_DATA_DOUBLE + override var high: Double = NO_DATA_DOUBLE + override var low: Double = NO_DATA_DOUBLE + override var last: Double = NO_DATA_DOUBLE + override var timestamp: Long = NO_DATA.toLong() + + companion object { + private const val NO_DATA_DOUBLE: Double = NO_DATA.toDouble() + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MarketDao.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MarketDao.kt new file mode 100644 index 00000000..bd7761fa --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MarketDao.kt @@ -0,0 +1,69 @@ +package com.aneonex.bitcoinchecker.tester.data.local + +import androidx.room.* +import com.aneonex.bitcoinchecker.tester.data.local.model.MarketEntity +import com.aneonex.bitcoinchecker.tester.data.local.model.MarketPairEntity +import com.aneonex.bitcoinchecker.tester.data.local.model.MarketWithPairs +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo + +interface MarketDaoBase { + // ------------- Market data ---------------------- + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMarket(market: MarketEntity): Long + + @Query("UPDATE markets SET updateDate = :updateDate WHERE id = :marketId") + suspend fun updateMarketDate(marketId: Long, updateDate: Long) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMarketPairs(marketPairs: List) + + @Suppress("SpellCheckingInspection") + @Transaction + @Query("SELECT * FROM markets WHERE marketKey = :marketKey COLLATE NOCASE") + suspend fun getMarketWithPairs(marketKey: String): MarketWithPairs? + + @Suppress("SpellCheckingInspection") + @Query("SELECT id FROM markets WHERE marketKey = :marketKey COLLATE NOCASE") + suspend fun getMarketId(marketKey: String): Long? + + @Query("DELETE FROM market_pairs WHERE marketId = :marketId") + suspend fun deleteMarketPairs(marketId: Long) +} + +@Dao +abstract class MarketDao: MarketDaoBase { + @Transaction + open suspend fun saveMarketWithPairs(marketKey: String, marketData: MyMarketPairsInfo): Long { + var marketId = getMarketId(marketKey) ?: 0L + + if(marketId != 0L) { + deleteMarketPairs(marketId) + updateMarketDate(marketId, marketData.lastSyncDate) + } else { + marketId = insertMarket( + MarketEntity( + id = marketId, + marketKey = marketKey, + updateDate = marketData.lastSyncDate + ) + ) + } + + marketData.pairs.also { pairs -> + insertMarketPairs( + pairs.map { + MarketPairEntity( + marketId = marketId, + baseAsset = it.currencyBase, + quoteAsset = it.currencyCounter, + contractType =it.contractType, + marketPairId = it.currencyPairId + ) + } + ) + } + + return marketId + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MarketDatabase.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MarketDatabase.kt new file mode 100644 index 00000000..727ac8f7 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MarketDatabase.kt @@ -0,0 +1,25 @@ +package com.aneonex.bitcoinchecker.tester.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.aneonex.bitcoinchecker.tester.data.local.MarketDatabase.Companion.VERSION +import com.aneonex.bitcoinchecker.tester.data.local.model.MarketEntity +import com.aneonex.bitcoinchecker.tester.data.local.model.MarketPairEntity + +@Database( + entities = [ + MarketEntity::class, + MarketPairEntity::class, + ], + version = VERSION, + exportSchema = true, +) + +abstract class MarketDatabase : RoomDatabase() { + abstract fun getMarketDao(): MarketDao + + companion object { + const val VERSION = 1 + const val DB_NAME = "MarketDatabase.db" + } +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MyMarketLocalDataSource.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MyMarketLocalDataSource.kt new file mode 100644 index 00000000..0effdda5 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/MyMarketLocalDataSource.kt @@ -0,0 +1,35 @@ +package com.aneonex.bitcoinchecker.tester.data.local + +import android.database.sqlite.SQLiteException +import androidx.annotation.WorkerThread +import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo +import com.aneonex.bitcoinchecker.tester.domain.exceptions.DatabaseError +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo +import javax.inject.Inject + +class MyMarketLocalDataSource @Inject constructor(private val appDao: MarketDao) { + + @WorkerThread + suspend fun getMarketData(marketKey: String): MyMarketPairsInfo? { + val marketData = appDao.getMarketWithPairs(marketKey) ?: return null + + return MyMarketPairsInfo( + marketData.market.updateDate, + marketData.pairs.map { CurrencyPairInfo( + it.baseAsset, + it.quoteAsset, + it.marketPairId, + it.contractType + ) }) + } + + @WorkerThread + suspend fun saveMarketData(marketKey: String, marketData: MyMarketPairsInfo) { + try { + appDao.saveMarketWithPairs(marketKey, marketData) + } catch (ex: SQLiteException) { + throw DatabaseError(ex) + } + } +} + diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketEntity.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketEntity.kt new file mode 100644 index 00000000..5afce85f --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketEntity.kt @@ -0,0 +1,21 @@ +package com.aneonex.bitcoinchecker.tester.data.local.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "markets", + indices = [Index("marketKey")] +) +data class MarketEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + + @ColumnInfo(collate = ColumnInfo.NOCASE) + val marketKey: String, + + @ColumnInfo + val updateDate: Long +) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketPairEntity.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketPairEntity.kt new file mode 100644 index 00000000..8abc66f3 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketPairEntity.kt @@ -0,0 +1,33 @@ +package com.aneonex.bitcoinchecker.tester.data.local.model + +import androidx.room.* +import com.aneonex.bitcoinchecker.datamodule.model.FuturesContractType + +@Entity( + tableName = "market_pairs", + foreignKeys = [ + ForeignKey( + entity = MarketEntity::class, + parentColumns = ["id"], + childColumns = ["marketId"], + onDelete = ForeignKey.CASCADE + ) + ], + primaryKeys = ["marketId", "baseAsset", "quoteAsset", "contractType"] +) +data class MarketPairEntity( + @ColumnInfo + val marketId: Long, + + @ColumnInfo + val baseAsset: String, + + @ColumnInfo + val quoteAsset: String, + + @ColumnInfo + val contractType: FuturesContractType, + + @ColumnInfo + val marketPairId: String?, +) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketWithPairs.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketWithPairs.kt new file mode 100644 index 00000000..31947f39 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/local/model/MarketWithPairs.kt @@ -0,0 +1,14 @@ +package com.aneonex.bitcoinchecker.tester.data.local.model + +import androidx.room.Embedded +import androidx.room.Relation + +data class MarketWithPairs ( + @Embedded val market: MarketEntity, + @Relation( + parentColumn = "id", + entityColumn = "marketId", + entity = MarketPairEntity::class + ) + val pairs: List +) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/HttpLoggerImpl.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/HttpLoggerImpl.kt new file mode 100644 index 00000000..32ab4c6f --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/HttpLoggerImpl.kt @@ -0,0 +1,17 @@ +package com.aneonex.bitcoinchecker.tester.data.remote + +import com.aneonex.bitcoinchecker.tester.data.HttpLogger +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import okhttp3.logging.HttpLoggingInterceptor + +internal class HttpLoggerImpl: HttpLoggingInterceptor.Logger, HttpLogger { + private val _messageFlow = MutableSharedFlow(1) + + override val messageFlow: SharedFlow + get() = _messageFlow + + override fun log(message: String) { + _messageFlow.tryEmit(message ) + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/MyMarketRemoteDataSource.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/MyMarketRemoteDataSource.kt new file mode 100644 index 00000000..5ef157ed --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/MyMarketRemoteDataSource.kt @@ -0,0 +1,204 @@ +package com.aneonex.bitcoinchecker.tester.data.remote + +import com.aneonex.bitcoinchecker.datamodule.model.* +import com.aneonex.bitcoinchecker.tester.data.TickerImpl +import com.aneonex.bitcoinchecker.tester.data.remote.util.await +import com.aneonex.bitcoinchecker.tester.domain.exceptions.* +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo +import kotlinx.coroutines.withTimeout +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import timber.log.Timber +import java.io.InvalidObjectException +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.math.max + +class MyMarketRemoteDataSource @Inject constructor( + private val httpClient: OkHttpClient +) { + + suspend fun fetchMarketCurrencyPairsInfo(market: Market): MyMarketPairsInfo { + val pairs: MutableList = ArrayList() + val numOfRequests = market.currencyPairsNumOfRequests + + val nextPairs: MutableList = ArrayList() + for (requestId in 0 until numOfRequests) { + try { + val nextUrl = market.getCurrencyPairsUrl(requestId) + val nextPostRequestBody = market.getCurrencyPairsPostRequestInfo(requestId) + + if (!nextUrl.isNullOrEmpty()) { +/* + val responseString = MarketHttp.httpClient.callMarket(nextUrl, nextPostRequestBody) { + timeout { + requestTimeoutMillis = 120_000 + socketTimeoutMillis = 120_000 + } + } +*/ + val responseString = httpClient + .newBuilder() + .callTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .build() + .callMarket(nextUrl, nextPostRequestBody) + + nextPairs.clear() + try { + market.parseCurrencyPairsMain(requestId, responseString, nextPairs) + } catch (ex: Exception) { + ex.rethrowIfCritical() + if (requestId == 0) { + throw ParseError(ex) + } + } + pairs.addAll(nextPairs) + } + } catch (ex: Exception) { + ex.rethrowIfCritical() + if (requestId == 0) { + throw ex + } + } + } + + pairs.sort() + + return MyMarketPairsInfo( + lastSyncDate = System.currentTimeMillis(), + pairs = pairs + ) + } + + suspend fun fetchMarketTicker(market: Market, checkerInfo: CheckerInfo): Ticker { + + val ticker = TickerImpl() + + updateMarketTicker( + ticker, + httpClient, + market, + 0, + checkerInfo + ) + + val numOfRequests = market.getNumOfRequests(checkerInfo) + if (numOfRequests > 1) { + // Executing additional requests + for (requestId in 1 until numOfRequests) { + try { + updateMarketTicker( + ticker, + httpClient, + market, + requestId, + checkerInfo + ) + + } catch (ex: Exception) { + ex.rethrowIfCritical() + // e.printStackTrace() + Timber.e(ex, "Failed to execute additional request #$requestId") + } + } + } + + return ticker // Success + } +} + +suspend fun OkHttpClient.callMarket(url: String, postRequestInfo: PostRequestInfo?): String { + // Prevent to hang OkHttp request + return withTimeout(max(callTimeoutMillis, 15_000) * 2 + 5_000L) { + callMarketInternal(url, postRequestInfo) + } +} + +private suspend fun OkHttpClient.callMarketInternal(url: String, postRequestInfo: PostRequestInfo?): String { + fun getResponseString(response: Response): String { + val responseString = response.body.string() + + if(!response.isSuccessful) + throw HttpMarketError(response.code, responseString) + + return responseString + } + + try { + val requestBuilder = Request.Builder().url(url) + + if (postRequestInfo == null) { + // logger.debug { "Market GET request: $url" } + // HTTP GET + val request = requestBuilder.build() + return getResponseString(this.newCall(request).await()) + } + + // HTTP POST + //logger.debug { "Market POST request: $url" } + + postRequestInfo.headers?.let { + it.forEach { (name, value) -> + requestBuilder.addHeader(name, value) + } + } + + val request = requestBuilder + .post(postRequestInfo.body.toRequestBody()) + .build() + return getResponseString(this.newCall(request).await()) + + } catch (ex: Exception) { + ex.rethrowIfCritical() + throw parseMarketError(ex) + } +} + +private suspend fun updateMarketTicker(ticker: Ticker, httpClient: OkHttpClient, market: Market, requestId: Int, checkerInfo: CheckerInfo) { + val url = market.getUrl(requestId, checkerInfo) + + if(url.isEmpty()) { + if(requestId > 0) + return + + throw IllegalArgumentException("Url is empty (market=${market.key})") + } + + val postRequestInfo = market.getPostRequestInfo(requestId, checkerInfo) + val responseString = httpClient.callMarket(url, postRequestInfo) + + if (responseString.isEmpty()) + throw UserFriendlyMarketError("Response data is empty") // TODO: Move to res + + try { + market.parseTickerMain(requestId, responseString, ticker, checkerInfo) + + if(ticker.last <= Ticker.NO_DATA) + throw InvalidObjectException("Parsed ticker has no data") + } catch (ex: MarketError){ + throw ex + } catch (ex: Exception){ + ex.rethrowIfCritical() + + val errorMessage: String? + + // Try to parse error message from response + try { + errorMessage = market.parseErrorMain( + requestId, + responseString, + checkerInfo + ) + } catch (ex2: Exception) { + ex2.rethrowIfCritical() + + // Failed to parse, re-throw original exception + throw ex + } + + throw UserFriendlyMarketError(errorMessage ?: "Unknown error (empty)") + } +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/util/Calls.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/util/Calls.kt new file mode 100644 index 00000000..690ce833 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/data/remote/util/Calls.kt @@ -0,0 +1,41 @@ +package com.aneonex.bitcoinchecker.tester.data.remote.util + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CompletionHandler +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +internal suspend fun Call.await(): Response { + return suspendCancellableCoroutine { continuation -> + val callback = ContinuationCallback(this, continuation) + enqueue(callback) + continuation.invokeOnCancellation(callback) + } +} + +private class ContinuationCallback( + private val call: Call, + private val continuation: CancellableContinuation +) : Callback, CompletionHandler { + + override fun onResponse(call: Call, response: Response) { + continuation.resume(response) + } + + override fun onFailure(call: Call, e: IOException) { + if (!call.isCanceled()) { + continuation.resumeWithException(e) + } + } + + override fun invoke(cause: Throwable?) { + try { + call.cancel() + } catch (_: Throwable) {} + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/LocalDataModule.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/LocalDataModule.kt new file mode 100644 index 00000000..dd4c3a22 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/LocalDataModule.kt @@ -0,0 +1,24 @@ +package com.aneonex.bitcoinchecker.tester.di + +import android.content.Context +import androidx.room.Room +import com.aneonex.bitcoinchecker.tester.data.local.MarketDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LocalDataModule { + + @Provides + @Singleton + fun provideMarketDatabase( + @ApplicationContext context: Context, + ): MarketDatabase = + Room.databaseBuilder(context, MarketDatabase::class.java, MarketDatabase.DB_NAME) + .build() +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/MarketModule.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/MarketModule.kt new file mode 100644 index 00000000..74bef435 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/MarketModule.kt @@ -0,0 +1,30 @@ +package com.aneonex.bitcoinchecker.tester.di + +import com.aneonex.bitcoinchecker.tester.data.MyMarketRepository +import com.aneonex.bitcoinchecker.tester.data.MyMarketRepositoryImpl +import com.aneonex.bitcoinchecker.tester.data.local.MarketDao +import com.aneonex.bitcoinchecker.tester.data.local.MarketDatabase +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Suppress("unused") +@Module +@InstallIn(SingletonComponent::class) +abstract class MarketModule { + + @Binds + abstract fun provideMyMarketRepository( + myMarketRepositoryImp: MyMarketRepositoryImpl + ): MyMarketRepository + + companion object { + + @Provides + fun provideMarketDao( + marketDatabase: MarketDatabase + ): MarketDao = marketDatabase.getMarketDao() + } +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/RemoteDataModule.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/RemoteDataModule.kt new file mode 100644 index 00000000..687d1734 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/di/RemoteDataModule.kt @@ -0,0 +1,36 @@ +package com.aneonex.bitcoinchecker.tester.di + +import com.aneonex.bitcoinchecker.tester.data.HttpLogger +import com.aneonex.bitcoinchecker.tester.data.remote.HttpLoggerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RemoteDataModule { + + @Singleton + @Provides + fun provideOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient = + OkHttpClient.Builder() + .addInterceptor(httpLoggingInterceptor) + .build() + + @Singleton + @Provides + fun provideHttpLoggingInterceptor(httpLogger: HttpLogger): HttpLoggingInterceptor = + HttpLoggingInterceptor(httpLogger as HttpLoggingInterceptor.Logger).apply { + level = HttpLoggingInterceptor.Level.BODY +// setLevel(HttpLoggingInterceptor.Level.BASIC) + } + + @Singleton + @Provides + fun provideHttpLoggerFlow(): HttpLogger = + HttpLoggerImpl() +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/dialog/DynamicCurrencyPairsDialog.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/dialog/DynamicCurrencyPairsDialog.kt deleted file mode 100644 index 8395901f..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/dialog/DynamicCurrencyPairsDialog.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.dialog - -import android.content.Context -import android.content.DialogInterface -import android.text.SpannableStringBuilder -import android.view.LayoutInflater -import androidx.appcompat.app.AlertDialog -import com.android.volley.NetworkResponse -import com.android.volley.RequestQueue -import com.android.volley.VolleyError -import com.aneonex.bitcoinchecker.datamodule.model.Market -import com.aneonex.bitcoinchecker.datamodule.util.CurrencyPairsMapHelper -import com.aneonex.bitcoinchecker.datamodule.util.FormatUtilsBase.formatSameDayTimeOrDate -import com.aneonex.bitcoinchecker.tester.R -import com.aneonex.bitcoinchecker.tester.databinding.DynamicCurrencyPairsDialogBinding -import com.aneonex.bitcoinchecker.tester.util.CheckErrorsUtils -import com.aneonex.bitcoinchecker.tester.util.HttpsHelper -import com.aneonex.bitcoinchecker.tester.volley.DynamicCurrencyPairsVolleyMainRequest -import com.aneonex.bitcoinchecker.tester.volley.generic.ResponseErrorListener -import com.aneonex.bitcoinchecker.tester.volley.generic.ResponseListener - -abstract class DynamicCurrencyPairsDialog protected constructor(context: Context, val market: Market, currencyPairsMapHelper: CurrencyPairsMapHelper?) : AlertDialog(context), DialogInterface.OnDismissListener { - private val binding = DynamicCurrencyPairsDialogBinding.inflate(LayoutInflater.from(context)) - private val requestQueue: RequestQueue = HttpsHelper.newRequestQueue(context) - private var currencyPairsMapHelper: CurrencyPairsMapHelper? - - override fun onDismiss(dialog: DialogInterface) { - requestQueue.cancelAll(this) - currencyPairsMapHelper = null - } - - private fun startRefreshing() { - setCancelable(false) - startRefreshingAnim() - val request = DynamicCurrencyPairsVolleyMainRequest(context, market, - object : ResponseListener() { - override fun onResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, response: CurrencyPairsMapHelper?) { - this@DynamicCurrencyPairsDialog.currencyPairsMapHelper = response - refreshStatusView(url, requestHeaders, networkResponse, responseString, null, null) - stopRefreshingAnim() - onPairsUpdated(market, currencyPairsMapHelper) - // dismiss(); - } - }, object : ResponseErrorListener() { - override fun onErrorResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, error: VolleyError) { - error.printStackTrace() - refreshStatusView(url, requestHeaders, networkResponse, responseString, CheckErrorsUtils.parseVolleyErrorMsg(context, error), error) - stopRefreshingAnim() - } - }) - request.tag = this - - requestQueue.add(request) - } - - private fun refreshStatusView(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, errorMsg: String?, error: VolleyError?) { - val dateString = if (currencyPairsMapHelper != null && currencyPairsMapHelper!!.date > 0) formatSameDayTimeOrDate(context, currencyPairsMapHelper!!.date) else context.getString(R.string.checker_add_dynamic_currency_pairs_dialog_last_sync_never) - binding.statusView.text = context.getString(R.string.checker_add_dynamic_currency_pairs_dialog_last_sync, dateString) - if (currencyPairsMapHelper != null && currencyPairsMapHelper!!.size > 0) binding.statusView.append(""" - - ${context.getString(R.string.checker_add_dynamic_currency_pairs_dialog_pairs, currencyPairsMapHelper!!.size)} - """.trimIndent()) - val ssb = SpannableStringBuilder() - if (errorMsg != null) { - ssb.append("\n") - ssb.append(context.getString(R.string.check_error_generic_prefix, errorMsg)) - } - CheckErrorsUtils.formatResponseDebug(context, ssb, url, requestHeaders, networkResponse, responseString, error) - binding.errorView.text = ssb - } - - private fun startRefreshingAnim() { - setCancelable(false) - binding.refreshImageView.isEnabled = false - } - - fun stopRefreshingAnim() { - setCancelable(true) - binding.refreshImageView.isEnabled = true - } - - abstract fun onPairsUpdated(market: Market, currencyPairsMapHelper: CurrencyPairsMapHelper?) - - init { - // setInverseBackgroundForced(true) - this.currencyPairsMapHelper = currencyPairsMapHelper - setTitle(R.string.checker_add_dynamic_currency_pairs_dialog_title) - setOnDismissListener(this) - setButton(BUTTON_NEUTRAL, context.getString(android.R.string.ok), null as DialogInterface.OnClickListener?) - - binding.refreshImageView.setOnClickListener { startRefreshing() } - refreshStatusView(null, null, null, null, null, null) - - setView(binding.root) - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/DatabaseError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/DatabaseError.kt new file mode 100644 index 00000000..fdad156b --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/DatabaseError.kt @@ -0,0 +1,3 @@ +package com.aneonex.bitcoinchecker.tester.domain.exceptions + +class DatabaseError(cause: Exception) : MarketError(cause) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/HttpMarketError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/HttpMarketError.kt new file mode 100644 index 00000000..e62c774c --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/HttpMarketError.kt @@ -0,0 +1,8 @@ +package com.aneonex.bitcoinchecker.tester.domain.exceptions + +class HttpMarketError(val httpCode: Int, val responseString: String?) : MarketError() { + override val message: String + get() = "HttpCode: $httpCode" +} + + diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/MarketError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/MarketError.kt new file mode 100644 index 00000000..88a1ed29 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/MarketError.kt @@ -0,0 +1,49 @@ +package com.aneonex.bitcoinchecker.tester.domain.exceptions + +import android.os.DeadSystemException +import java.io.IOException +import java.io.InterruptedIOException +import java.util.concurrent.CancellationException + +open class MarketError : Exception { + constructor() : super() + constructor(message: String?) : super(message) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) +} + +// Rethrow coroutine cancellation exception +fun Exception.rethrowIfCritical() { +// if(this.javaClass == TimeoutCancellationException::class.java) +// return + + if(this is CancellationException // For OkHttp + || this is DeadSystemException // The system is going to reboot or shutdown + ) { + throw this + } +/* + // For ktor + if(this.javaClass == CancellationException::class.java) + throw this + */ +} + +fun parseMarketError(ex: Throwable): MarketError { + return when(ex) { + is MarketError -> + ex + +// is HttpRequestTimeoutException, +// is SocketTimeoutException, +// is ConnectTimeoutException, + //is TimeoutCancellationException, + is InterruptedIOException -> + TimeoutError(ex) + + is IOException -> // SocketException, UnknownHostException + NetworkError(ex) + + else -> UnknownMarketError(ex) + } +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/NetworkError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/NetworkError.kt new file mode 100644 index 00000000..50d679df --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/NetworkError.kt @@ -0,0 +1,5 @@ +package com.aneonex.bitcoinchecker.tester.domain.exceptions + +import com.aneonex.bitcoinchecker.tester.domain.exceptions.MarketError + +class NetworkError(cause: Throwable) : MarketError(cause) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/ParseError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/ParseError.kt new file mode 100644 index 00000000..826799dc --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/ParseError.kt @@ -0,0 +1,3 @@ +package com.aneonex.bitcoinchecker.tester.domain.exceptions + +open class ParseError(cause: Throwable?) : MarketError(cause) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/TimeoutError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/TimeoutError.kt new file mode 100644 index 00000000..caac4c1a --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/TimeoutError.kt @@ -0,0 +1,3 @@ +package com.aneonex.bitcoinchecker.tester.domain.exceptions + +class TimeoutError(cause: Throwable) : MarketError(cause) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/UnknownMarketError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/UnknownMarketError.kt new file mode 100644 index 00000000..b79aeff6 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/UnknownMarketError.kt @@ -0,0 +1,3 @@ +package com.aneonex.bitcoinchecker.tester.domain.exceptions + +class UnknownMarketError(cause: Throwable?) : MarketError(cause) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/UserFriendlyMarketError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/UserFriendlyMarketError.kt new file mode 100644 index 00000000..052e22be --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/exceptions/UserFriendlyMarketError.kt @@ -0,0 +1,3 @@ +package com.aneonex.bitcoinchecker.tester.domain.exceptions + +class UserFriendlyMarketError(message: String) : MarketError(message) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MarketTickerResult.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MarketTickerResult.kt new file mode 100644 index 00000000..9ea50b4e --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MarketTickerResult.kt @@ -0,0 +1,10 @@ +package com.aneonex.bitcoinchecker.tester.domain.model + +import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo +import com.aneonex.bitcoinchecker.datamodule.model.Ticker + +class MarketTickerResult( + val ticker: Ticker, + val pairInfo: CheckerInfo, + val error: String? = null +) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MyMarket.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MyMarket.kt new file mode 100644 index 00000000..38d2942a --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MyMarket.kt @@ -0,0 +1,6 @@ +package com.aneonex.bitcoinchecker.tester.domain.model + +data class MyMarket ( + val key: String, + val name: String, +) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MyMarketPairsInfo.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MyMarketPairsInfo.kt new file mode 100644 index 00000000..3c08ed72 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/domain/model/MyMarketPairsInfo.kt @@ -0,0 +1,42 @@ +package com.aneonex.bitcoinchecker.tester.domain.model + +import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo +import com.aneonex.bitcoinchecker.datamodule.model.FuturesContractType + +data class MyMarketPairsInfo( + val lastSyncDate: Long = 0, + val pairs: List = emptyList(), +) { + val size: Int get() = pairs.size + + val baseCurrencies: Iterable get() = + pairs + .map { it.currencyBase } + .distinct() + + fun getQuoteCurrencies(baseCurrency: String): Iterable = + pairs + .filter { it.currencyBase == baseCurrency } + .map { it.currencyCounter } + .distinct() + + fun getAvailableFuturesContractsTypes(baseCurrency: String?, quoteCurrency: String?): List { + if(baseCurrency == null || quoteCurrency == null) return emptyList() + + return pairs + .filter { + it.contractType != FuturesContractType.NONE + && it.currencyBase == baseCurrency + && it.currencyCounter == quoteCurrency } + .map { it.contractType } + } + + fun getCurrencyPairInfo(baseCurrency: String, quoteCurrency: String, contractType: FuturesContractType): CurrencyPairInfo? { + // TODO: Optimize performance + return pairs.firstOrNull { + it.currencyBase == baseCurrency + && it.currencyCounter == quoteCurrency + && it.contractType == contractType + } + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/MainActivity.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/MainActivity.kt new file mode 100644 index 00000000..eab9c33b --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/MainActivity.kt @@ -0,0 +1,20 @@ +package com.aneonex.bitcoinchecker.tester.ui + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import com.aneonex.bitcoinchecker.tester.ui.theme.MyAppTheme +import com.aneonex.bitcoinchecker.tester.ui.navigation.MyAppNavHost +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MyAppTheme { + MyAppNavHost() + } + } + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/ComboBox.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/ComboBox.kt new file mode 100644 index 00000000..5fded32e --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/ComboBox.kt @@ -0,0 +1,172 @@ +package com.aneonex.bitcoinchecker.tester.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ComboBox( + modifier: Modifier = Modifier, + selectedIndex: Int = -1, + itemList: List, + onValueChange: (Int) -> Unit, + label: String? = null +) { + var expanded by remember { mutableStateOf(false) } +// var selectedIndex by remember { mutableStateOf(initialIndex) } +// var selectedIndex = initialIndex + + ExposedDropdownMenuBox( + expanded = expanded, + modifier = modifier, + onExpandedChange = { + expanded = !expanded + }) { + OutlinedTextField( + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), +// .widthIn(min = 10.dp) +// .defaultMinSize(minWidth = 1.dp), +// modifier = modifier, + singleLine = true, + maxLines = 1, + readOnly = true, + value = if(selectedIndex >= 0) itemList[selectedIndex] else "", +// color = MaterialTheme.colors.primary, +// placeholder = {Text("Select market")}, + onValueChange = { }, + label = { if(!label.isNullOrEmpty()) Text(label) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + }, +// colors = ExposedDropdownMenuDefaults.textFieldColors( +// textColor = MaterialTheme.colors.onSurface +// ) + ) + ExposedDropdownMenu( +// modifier = modifier, +// .fillMaxWidth(), +// .exposedDropdownSize(), + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + + val listState = rememberLazyListState() + // Remember a CoroutineScope to be able to launch + val coroutineScope = rememberCoroutineScope() + + val lazyHeight = (itemList.size).coerceAtMost(10) * 48 + +// itemList.forEachIndexed { index, itemText -> + LazyColumn( + state = listState, + modifier = Modifier + .simpleVerticalScrollbar(state = listState) + .size(400.dp, lazyHeight.dp) // Required to fix intrinsic issue + ) { + itemsIndexed(itemList) { index, itemText -> +// val itemText = itemList[index] + DropdownMenuItem( + modifier = Modifier +// .size(400.dp, 200.dp) +// .fillMaxWidth() + .let { + if (selectedIndex == index) + it.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)) + else it + }, + onClick = { + if (selectedIndex != index) { + //selectedIndex = index + onValueChange(index) + } + expanded = false + }, + text = { + Text(text = itemText) + } + ) + } + } + + LaunchedEffect(coroutineScope){ + val scrollIndexShift = 2 + if(selectedIndex > scrollIndexShift) { + listState.scrollToItem(selectedIndex - scrollIndexShift) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun ComboBoxPreview(sampleCount: Int = 3) { + Column( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + ) { + Row { + (1..sampleCount).forEach { + ComboBox( + modifier = Modifier +// .widthIn(20.dp, 200.dp) + .weight(1f) + , + + itemList = listOf("Item1", "Item2"), + label = "Test_$it", + onValueChange = {} + ) + } + } + Row { + ComboBox( + modifier = + Modifier + .weight(1f) +// .fillMaxWidth() + .widthIn(max = 100.dp) + , + itemList = listOf("Item1", "Item2"), + label = "Test", + onValueChange = {} + ) + } + + Row { + (1..sampleCount).forEach { +// Column(Modifier.weight(1f)) { + OutlinedTextField( + modifier = Modifier + .weight(1f) + .padding(4.dp) +// .width(50.dp) +// .widthIn(10.dp, 200.dp) +// .defaultMinSize(minWidth = 1.dp) + , + value = "Label $it", + singleLine = true, + onValueChange = {}, + ) + } + // } + } + + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/LogBox.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/LogBox.kt new file mode 100644 index 00000000..0b3da434 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/LogBox.kt @@ -0,0 +1,46 @@ +package com.aneonex.bitcoinchecker.tester.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun LogBox(logText: String) { + Column { + Text( + text = "Logs", + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.tertiary) + .padding(4.dp), + + color = MaterialTheme.colorScheme.onTertiary, + style = MaterialTheme.typography.titleMedium + + ) + + BasicTextField( + value = logText, + onValueChange = {}, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.tertiaryContainer) + .padding(4.dp), + readOnly = true, + textStyle = MaterialTheme.typography.bodySmall, + singleLine = false, + ) + } +} + +@Preview +@Composable +private fun LogBoxPreview() { + LogBox(logText = "My text 1\nLine 222") +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/Scrollbar.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/Scrollbar.kt new file mode 100644 index 00000000..7b26b053 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/Scrollbar.kt @@ -0,0 +1,51 @@ +package com.aneonex.bitcoinchecker.tester.ui.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +// Source: https://stackoverflow.com/questions/66341823/jetpack-compose-scrollbars +fun Modifier.simpleVerticalScrollbar( + state: LazyListState, + width: Dp = 8.dp +): Modifier = composed { + val targetAlpha = if (state.isScrollInProgress) 1f else 0f + val duration = if (state.isScrollInProgress) 150 else 1000 + + val alpha by animateFloatAsState( + targetValue = targetAlpha, + animationSpec = tween(durationMillis = duration) + ) + + val barColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + + drawWithContent { + drawContent() + + val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index + val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0f + + // Draw scrollbar if scrolling or if the animation is still running and lazy column has content + if (needDrawScrollbar && firstVisibleElementIndex != null) { + val elementHeight = this.size.height / state.layoutInfo.totalItemsCount + val scrollbarOffsetY = firstVisibleElementIndex * elementHeight + val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight + + drawRect( + color = barColor, + topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY), + size = Size(width.toPx(), scrollbarHeight), + alpha = alpha + ) + } + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/Ticker.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/Ticker.kt new file mode 100644 index 00000000..694929cb --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/components/Ticker.kt @@ -0,0 +1,104 @@ +package com.aneonex.bitcoinchecker.tester.ui.components + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.aneonex.bitcoinchecker.datamodule.model.Ticker +import com.aneonex.bitcoinchecker.datamodule.util.FormatUtilsBase +import com.aneonex.bitcoinchecker.tester.R + +@Composable +fun Ticker( + timestamp: Long, + last: Double, + + high: Double, + low: Double, + + ask: Double, + bid: Double, + + volBase: Double, + volQuote: Double, + + currencyBase: String, + currencyQuote: String, +) { + val spaceSize = 6.dp + + Column { + TitleValueItem( + R.string.ticker_timestamp, + FormatUtilsBase.formatSameDayTimeOrDate(LocalContext.current, timestamp)) + + PriceItem(R.string.ticker_last, last, currencyQuote) + + if(high > Ticker.NO_DATA) { + Spacer(modifier = Modifier.size(spaceSize)) + PriceItem(R.string.ticker_high, high, currencyQuote) + PriceItem(R.string.ticker_low, low, currencyQuote) + } + + if(ask > Ticker.NO_DATA) { + Spacer(modifier = Modifier.size(spaceSize)) + PriceItem(R.string.ticker_ask, ask, currencyQuote) + PriceItem(R.string.ticker_bid, bid, currencyQuote) + } + + Spacer(modifier = Modifier.size(spaceSize)) + if(volBase > Ticker.NO_DATA) + PriceItem(R.string.ticker_vol_base, volBase, currencyBase) + if(volQuote > Ticker.NO_DATA) + PriceItem(R.string.ticker_vol_quote, volQuote, currencyQuote) + } +} + +@Composable +private fun PriceItem(@StringRes titleId: Int, price: Double, currency: String) { + TitleValueItem(titleId = titleId, value = FormatUtilsBase.formatPriceWithCurrency(price, currency)) +} + +@Composable +private fun TitleValueItem(@StringRes titleId: Int, value: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( +// style = Typography.bodyMedium, + text = stringResource(id = titleId) + ":" + ) + Text( + text = value, +// style = Typography.bodySmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 4.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun TickerPreview(){ + Ticker( + timestamp = 1668282179000, + last = 18521.18, + + high = 20011.55, + low = 16544.2, + + ask = 18622.3456, + bid = 14633.0, + + volBase = 100_000.0, + volQuote = 200_000_000.555, + + currencyBase = "BTC", + currencyQuote = "USDT" + ) +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/error/ErrorScreen.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/error/ErrorScreen.kt new file mode 100644 index 00000000..020b1f85 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/error/ErrorScreen.kt @@ -0,0 +1,97 @@ +package com.aneonex.bitcoinchecker.tester.ui.features.error + +import android.content.Context +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.aneonex.bitcoinchecker.tester.R +import com.aneonex.bitcoinchecker.tester.ui.theme.* +import java.io.IOException +import java.net.SocketTimeoutException + +class ErrorScreenViewState(private val throwable: Throwable) { + + @DrawableRes + fun getErrorImage(): Int = when (throwable) { + is IOException -> R.drawable.ic_no_connection + else -> R.drawable.ic_error + } + + fun getErrorMessage(context: Context): String { + var message = when (throwable) { +// is HttpException -> throwable.message() + is SocketTimeoutException -> context.getString(R.string.timeout_error_message) + is IOException -> context.getString(R.string.no_internet_connection) + else -> context.getString(R.string.something_went_wrong) + } + throwable.message?.also { + message = "$message\n($it)" + } + + return message + } +} + +@Composable +fun ErrorScreen( + modifier: Modifier = Modifier, + errorScreenViewState: ErrorScreenViewState, + onTryAgainButtonClick: () -> Unit +) { + Surface(modifier = modifier) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = errorScreenViewState.getErrorImage()), + contentDescription = errorScreenViewState.getErrorMessage(LocalContext.current) + ) + + Text( + text = errorScreenViewState.getErrorMessage(LocalContext.current), + style = Typography.bodyLarge, + color = Gray700, + modifier = Modifier.padding(top = 16.dp) + ) + Button( + onClick = onTryAgainButtonClick, + modifier = Modifier + .width(250.dp) + .padding(top = 16.dp) +// .background(Gray900, RoundedCornerShape(4.dp)) + ) { + Text( + text = stringResource(id = R.string.try_again), + //style = Typography.labelLarge, + //color = White + ) + } + } + } + +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) +@Composable +fun ErrorScreenPreview() { + MyAppTheme { + ErrorScreen(errorScreenViewState = ErrorScreenViewState(IOException())) {} + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/loading/LoadingScreen.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/loading/LoadingScreen.kt new file mode 100644 index 00000000..a3934239 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/loading/LoadingScreen.kt @@ -0,0 +1,38 @@ +package com.aneonex.bitcoinchecker.tester.ui.features.loading + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.aneonex.bitcoinchecker.tester.ui.theme.MyAppTheme + +@Composable +fun LoadingScreen(modifier: Modifier = Modifier) { +/* + var progress by remember { mutableStateOf(0.1f) } + val animatedProgress = animateFloatAsState( + targetValue = progress, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec + ).value +*/ + Surface(modifier = modifier.fillMaxSize()) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + //LoadingAnimation(modifier = Modifier.size(75.dp)) + CircularProgressIndicator(/*progress = animatedProgress*/) + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun LoadingScreenPreview() { + MyAppTheme { + LoadingScreen() + } +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestScreen.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestScreen.kt new file mode 100644 index 00000000..c6ebbcd5 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestScreen.kt @@ -0,0 +1,328 @@ +package com.aneonex.bitcoinchecker.tester.ui.features.markettest + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo +import com.aneonex.bitcoinchecker.datamodule.model.FuturesContractType +import com.aneonex.bitcoinchecker.tester.R +import com.aneonex.bitcoinchecker.tester.data.TickerImpl +import com.aneonex.bitcoinchecker.tester.domain.model.MarketTickerResult +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarket +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo +import com.aneonex.bitcoinchecker.tester.ui.components.ComboBox +import com.aneonex.bitcoinchecker.tester.ui.components.LogBox +import com.aneonex.bitcoinchecker.tester.ui.components.Ticker +import com.aneonex.bitcoinchecker.tester.ui.features.error.ErrorScreen +import com.aneonex.bitcoinchecker.tester.ui.features.error.ErrorScreenViewState +import com.aneonex.bitcoinchecker.tester.ui.features.loading.LoadingScreen +import com.aneonex.bitcoinchecker.tester.ui.features.markettest.dto.MarketPairsUpdateState +import com.aneonex.bitcoinchecker.tester.ui.features.syncpairs.SyncPairsDialog +import com.aneonex.bitcoinchecker.tester.ui.theme.Typography +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber + +@Composable +fun MarketTestScreen(marketViewModel: MarketTestViewModel = hiltViewModel()) { + + val uiState by marketViewModel.uiState.collectAsState() + + when (uiState) { + is MarketTestUiState.Success -> { + + val viewState = (uiState as MarketTestUiState.Success).data + + MarketScreenMain( + viewState = viewState, + httpLogText = marketViewModel.httpLogText, + + onMarketChanged = marketViewModel::setCurrentMarket, + + onBaseAssetChanged = marketViewModel::setCurrentBaseAsset, + onQuoteAssetChanged = marketViewModel::setCurrentQuoteAsset, + onContractTypeChanged = marketViewModel::setCurrentContractType, + + onTestMarketButtonClick = marketViewModel::getTicker, + onSyncCurrencyPairsClick = marketViewModel::syncCurrencyPairs, + onSyncCurrencyPairsDialogClosed = marketViewModel::clearCurrencyPairsUpdateError + ) + } + + is MarketTestUiState.Error -> { + ErrorScreen(errorScreenViewState = ErrorScreenViewState((uiState as MarketTestUiState.Error).exception)) { + marketViewModel.retryLoadMarketList() + } + } + is MarketTestUiState.Loading -> { + LoadingScreen() + } + } +} + +@Composable +private fun MarketScreenMain( + viewState: MarketTestScreenViewState, + httpLogText: StateFlow, + + onMarketChanged: (MyMarket?) -> Unit, + + onBaseAssetChanged: (String?) -> Unit, + onQuoteAssetChanged: (String?) -> Unit, + onContractTypeChanged: (FuturesContractType?) -> Unit, + + onTestMarketButtonClick: (MyMarket) -> Unit, + onSyncCurrencyPairsClick: () -> Unit, + onSyncCurrencyPairsDialogClosed: () -> Unit, +) { + val basePadding = 8.dp + + val logText by httpLogText.collectAsState() + + val markets = viewState.markets + val marketKeys = markets.map { market -> market.key }.also { + Timber.d("XXX", "***** 2) REMAP MARKETS ******") + } + Timber.d("XXX", "***** 1) INIT MarketScreenMain ******") + + val currentMarket by viewState.currentMarket.collectAsState() + val currentMarketIndex = markets.indexOf(currentMarket).also { + Timber.d("XXX", "***** 3) REMAP MARKET INDEX ******") + } + + val canUpdatePairs by viewState.canUpdatePairs.collectAsState() + val currentMarketPairsInfo by viewState.currentMarketPairsInfo.collectAsState() + + val currentBaseAsset by viewState.currentBaseAsset.collectAsState() + val currentQuoteAsset by viewState.currentQuoteAsset.collectAsState() + val currentContractType by viewState.currentContractType.collectAsState() + + val baseAssets by viewState.baseAssets.collectAsState() + val quoteAssets by viewState.quoteAssets.collectAsState() + val contactTypes by viewState.contractTypes.collectAsState() + + val marketTicker by viewState.marketTicker.collectAsState() + val marketPairsUpdateState by viewState.marketPairsUpdateState.collectAsState() + + val showSyncPairsDialog = remember { mutableStateOf(false) } + + if(showSyncPairsDialog.value){ + currentMarketPairsInfo?.also { + SyncPairsDialog( + it, + marketPairsUpdateState, + onSyncClick = onSyncCurrencyPairsClick, + onDismiss = { + showSyncPairsDialog.value = false + onSyncCurrencyPairsDialogClosed() + } + ) + } + } + + val scrollState = rememberScrollState() + + Surface { + Box(modifier = Modifier + .padding(basePadding) + .verticalScroll(scrollState)) { + Column( + modifier = Modifier + .fillMaxSize(), + ) { + + Row(Modifier.padding(bottom = basePadding)) { + ComboBox( + modifier = Modifier + //.fillMaxWidth() + .weight(1f) + , + itemList = marketKeys, + selectedIndex = currentMarketIndex, + label = stringResource(id = R.string.market_screen_market), + onValueChange = { marketIndex -> + onMarketChanged(markets[marketIndex]) + }) + } + + Row ( + modifier = Modifier.padding(bottom = basePadding), + verticalAlignment = Alignment.CenterVertically + ) { + + if(baseAssets.isEmpty()) { + Text( + modifier = Modifier + .weight(4f), + text = stringResource(id = R.string.checker_add_check_currency_empty_warning_title), + textAlign = TextAlign.Center + ) + } else { + + ComboBox( + modifier = Modifier + .weight(2f), + itemList = baseAssets, + selectedIndex = baseAssets.indexOf(currentBaseAsset), + label = stringResource(id = R.string.market_screen_base), + onValueChange = { index -> + onBaseAssetChanged(baseAssets[index]) + } + ) + + Text( + text = "/", + fontSize = 30.sp, + modifier = Modifier.padding( + start = basePadding, + end = basePadding + ) + ) + + ComboBox( + modifier = Modifier.weight(2f), + itemList = quoteAssets, + selectedIndex = quoteAssets.indexOf(currentQuoteAsset), + label = stringResource(id = R.string.market_screen_quote), + onValueChange = { index -> + onQuoteAssetChanged(quoteAssets[index]) + } + ) + } + Spacer(modifier = Modifier.size(basePadding)) + + Button( + onClick = { + showSyncPairsDialog.value = true + }, + enabled = canUpdatePairs + ) { + Text(text = stringResource(id = R.string.market_screen_sync)) + } + } + + if(contactTypes.isNotEmpty()) { + Row( + modifier = Modifier.padding(bottom = basePadding), + ) { + ComboBox( + itemList = contactTypes.map { it.toString() }, + selectedIndex = contactTypes.indexOf(currentContractType), + label = stringResource(id = R.string.market_screen_contract_type), + onValueChange = { index -> + onContractTypeChanged(contactTypes[index]) + } + ) + } + } + + Row( + modifier = Modifier.padding(bottom = basePadding), + ) { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + currentMarket?.also { + onTestMarketButtonClick(it) + } + }) { + Text( + text = stringResource(id = R.string.market_screen_get_price), + style = Typography.labelLarge, + ) + } + } + + marketTicker + .also { marketTickerValue -> + if (marketTickerValue == null) { + Text(text = stringResource(id = R.string.generic_none)) + } else { + if (marketTickerValue.error != null) { + Text(text = "Error: ${marketTickerValue.error}") + } else { + with(marketTickerValue.ticker) { + Ticker( + timestamp = timestamp, + last = last, + + high = high, + low = low, + + ask = ask, + bid = bid, + + volBase = vol, + volQuote = volQuote, + + currencyBase = marketTickerValue.pairInfo.currencyBase, + currencyQuote = marketTickerValue.pairInfo.currencyCounter, + ) + } + } + } + } + + Spacer(modifier = Modifier.size(basePadding * 2)) + LogBox(logText) + } + } + } +} + +/* +@Preview(showBackground = true) +@Composable +private fun MarketScreenPreview() { + MarketTestScreen() +} +*/ + +@Preview(showBackground = true) +@Composable +private fun MarketScreenMainPreview() { + val ticker = TickerImpl() + ticker.last = 123.46 + + val checkerInfo = CheckerInfo("BTC", "USD", null, FuturesContractType.NONE) + + MarketScreenMain( + MarketTestScreenViewState( + listOf(), + MutableStateFlow(null), + MutableStateFlow(true), + + MutableStateFlow(null), + MutableStateFlow(MarketPairsUpdateState()), + + MutableStateFlow("BTC"), + MutableStateFlow("USD"), + MutableStateFlow(FuturesContractType.PERPETUAL), + + MutableStateFlow(listOf("BTC", "ETH")), + MutableStateFlow(listOf("EUR", "USD")), + MutableStateFlow(listOf(FuturesContractType.MONTHLY, FuturesContractType.PERPETUAL)), + + MutableStateFlow(MarketTickerResult(ticker, checkerInfo)) + ), + MutableStateFlow("My test message\nNew line 123"), + + onMarketChanged = {}, + onBaseAssetChanged = {}, + onQuoteAssetChanged = {}, + onContractTypeChanged = {}, + onTestMarketButtonClick = {}, + onSyncCurrencyPairsClick = {}, + onSyncCurrencyPairsDialogClosed = {} + ) +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestScreenViewState.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestScreenViewState.kt new file mode 100644 index 00000000..b2c412c1 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestScreenViewState.kt @@ -0,0 +1,31 @@ +package com.aneonex.bitcoinchecker.tester.ui.features.markettest + +import com.aneonex.bitcoinchecker.datamodule.model.FuturesContractType +import com.aneonex.bitcoinchecker.tester.domain.model.MarketTickerResult +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarket +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo +import com.aneonex.bitcoinchecker.tester.ui.features.markettest.dto.MarketPairsUpdateState +import kotlinx.coroutines.flow.StateFlow + +class MarketTestScreenViewState( + val markets: List, + val currentMarket: StateFlow, + val canUpdatePairs: StateFlow, + + val currentMarketPairsInfo: StateFlow, + val marketPairsUpdateState: StateFlow, + + val currentBaseAsset: StateFlow, + val currentQuoteAsset: StateFlow, + val currentContractType: StateFlow, + + val baseAssets: StateFlow>, + val quoteAssets: StateFlow>, + val contractTypes: StateFlow>, + + val marketTicker: StateFlow +) { + +// fun getMarkets() = marketList + +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestViewModel.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestViewModel.kt new file mode 100644 index 00000000..649b2acd --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/MarketTestViewModel.kt @@ -0,0 +1,277 @@ +package com.aneonex.bitcoinchecker.tester.ui.features.markettest + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo +import com.aneonex.bitcoinchecker.datamodule.model.FuturesContractType +import com.aneonex.bitcoinchecker.tester.data.HttpLogger +import com.aneonex.bitcoinchecker.tester.data.MyMarketRepository +import com.aneonex.bitcoinchecker.tester.domain.exceptions.MarketError +import com.aneonex.bitcoinchecker.tester.domain.model.MarketTickerResult +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarket +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo +import com.aneonex.bitcoinchecker.tester.ui.features.markettest.dto.MarketPairsUpdateState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class MarketTestViewModel @Inject constructor( + private val myMarketRepository: MyMarketRepository, + private val httpLogger: HttpLogger +) : ViewModel() { + private val _marketTicker = MutableStateFlow(null) + + private val _currentMarket = MutableStateFlow(null) + + private val _marketPairsUpdateTrigger = MutableSharedFlow(1) + private val _marketPairsUpdateState = MutableStateFlow(MarketPairsUpdateState()) + + private val _currentBaseAsset = MutableStateFlow(null) + private val _currentQuoteAsset = MutableStateFlow(null) + private val _currentContractType = MutableStateFlow(null) + + private val _reloadMarketsTrigger = MutableSharedFlow(1) + + private var _currentMarketPairsInfo: StateFlow = _currentMarket + .combine(_marketPairsUpdateTrigger){ market, _ -> market } + .map { market -> + if (market == null) null + else myMarketRepository.getMarketCurrencyPairsInfo(market) + } + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + + private var _currentMarketCanUpdatePairs: StateFlow = _currentMarket + .map { market -> + if (market == null) false + else myMarketRepository.isMarketSupportsUpdatePairs(market) + } + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + + private var _syncPairsJob: Job? = null + + private var _baseAssets: StateFlow> = _currentMarketPairsInfo + .map { pairsInfo -> pairsInfo?.baseCurrencies?.toList() ?: emptyList() } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + private var _quoteAssets: StateFlow> = _currentMarketPairsInfo + .combine(_currentBaseAsset) { pairsInfo, baseAsset -> + if (baseAsset == null || pairsInfo == null) + emptyList() + else + pairsInfo.getQuoteCurrencies(baseAsset).toList() + } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + private var _contractTypes: StateFlow> = + combine( + _currentMarketPairsInfo, + _currentBaseAsset, + _currentQuoteAsset, + ) + { pairsInfo, baseCurrency, quoteCurrency -> + if (baseCurrency == null || quoteCurrency == null || pairsInfo == null) + emptyList() + else + pairsInfo.getAvailableFuturesContractsTypes(baseCurrency, quoteCurrency) + } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + private var _currentMarketPair: StateFlow = + combine( + _currentMarketPairsInfo, + _currentBaseAsset, + _currentQuoteAsset, + _currentContractType + ) { + pairs, baseCurrency, quoteCurrency, futuresContractType -> + if(pairs != null && baseCurrency != null && quoteCurrency != null ) + pairs.getCurrencyPairInfo(baseCurrency, quoteCurrency, futuresContractType ?: FuturesContractType.NONE) + else + null + } + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + + init { + viewModelScope.launch { + _marketPairsUpdateTrigger.emit(Unit) + _reloadMarketsTrigger.emit(Unit) + + launch { + _baseAssets.collectLatest { newBaseAssets -> + if( _currentBaseAsset.value == null || !newBaseAssets.contains(_currentBaseAsset.value)) { + _currentBaseAsset.value = newBaseAssets.firstOrNull() + } + } + } + + launch { + _quoteAssets.collectLatest { newQuoteAssets -> + if( _currentQuoteAsset.value == null || !newQuoteAssets.contains(_currentQuoteAsset.value)) { + _currentQuoteAsset.value = newQuoteAssets.firstOrNull() + } + } + } + + launch { + _contractTypes.collectLatest { contractTypes -> + if( _currentContractType.value == null || !contractTypes.contains(_currentContractType.value)) { + _currentContractType.value = contractTypes.firstOrNull() + } + } + } + + launch { + _currentMarketPair.collectLatest { + _marketTicker.value = null + } + } + + // Handle HTTP log messages + launch { + val lineMaxLength = 400 + val messageCountLimit = 10 + + val messageList = mutableListOf() + httpLogger.messageFlow.collect { + if(messageList.size == messageCountLimit){ + messageList.removeAt(0) + } + + fun formatLogLine(line: String): String{ + if(line.length <= lineMaxLength) + return line + return line.take(lineMaxLength) + "..." + } + + messageList.add(formatLogLine(it)) + _httpLogText.value = messageList.joinToString("\n") + } + } + } + } + + private val _httpLogText = MutableStateFlow("") + val httpLogText = _httpLogText.asStateFlow() + + val uiState: StateFlow = _reloadMarketsTrigger + .map { + val marketList = + try { + myMarketRepository.getMarketList().sortedBy { x -> x.name.lowercase() } + } catch (e: Exception) { + return@map MarketTestUiState.Error(e) + } + + _currentMarket.value.also { + if(it == null || !marketList.contains(it)) { + _currentMarket.value = marketList.firstOrNull() + } + } + + MarketTestUiState.Success( + MarketTestScreenViewState( + markets = marketList, + currentMarket = _currentMarket.asStateFlow(), + canUpdatePairs = _currentMarketCanUpdatePairs, + + currentMarketPairsInfo = _currentMarketPairsInfo, + marketPairsUpdateState = _marketPairsUpdateState.asStateFlow(), + + currentBaseAsset = _currentBaseAsset.asStateFlow(), + currentQuoteAsset = _currentQuoteAsset.asStateFlow(), + currentContractType = _currentContractType.asStateFlow(), + + baseAssets = _baseAssets, + quoteAssets = _quoteAssets, + contractTypes = _contractTypes, + + marketTicker = _marketTicker.asStateFlow() + ) + ) + } +// .catch { e -> +// emit(UiState.Error(e)) +// } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + //started = SharingStarted.Eagerly, + initialValue = MarketTestUiState.Loading + ) + + fun getTicker(market: MyMarket) { + _currentMarketPair.value?.also { + viewModelScope.launch { + try { + _marketTicker.value = myMarketRepository.getMarketTicker(market, it) + } catch (e: Exception) { + _marketTicker.value = null + } + } + } + } + + fun setCurrentMarket(market: MyMarket?) { + _currentMarket.value = market + } + + fun setCurrentBaseAsset(asset: String?) { + _currentBaseAsset.value = asset + } + + fun setCurrentQuoteAsset(asset: String?) { + _currentQuoteAsset.value = asset + } + + fun setCurrentContractType(contractType: FuturesContractType?) { + _currentContractType.value = contractType + } + + @Synchronized + fun syncCurrencyPairs() { + _syncPairsJob?.cancel() + _syncPairsJob = null + + _currentMarket.value?.also { market -> + _marketPairsUpdateState.value = MarketPairsUpdateState(true) + _syncPairsJob = viewModelScope.launch { + Timber.d("Start market sync: ${market.key}") + try { + myMarketRepository.updateMarketCurrencyPairs(market) + _marketPairsUpdateTrigger.emit(Unit) + _marketPairsUpdateState.value = MarketPairsUpdateState() + } catch (ex: MarketError) { + _marketPairsUpdateState.value = MarketPairsUpdateState(error = ex.message) + } + }.also { + it.invokeOnCompletion { exception: Throwable? -> + Timber.d(exception, "Market sync completed: %s", market.key) + } + } + } + } + + fun clearCurrencyPairsUpdateError() { + _syncPairsJob?.cancel() + _syncPairsJob = null + + _marketPairsUpdateState.value = MarketPairsUpdateState(false) + } + + fun retryLoadMarketList() { + viewModelScope.launch { + _reloadMarketsTrigger.emit(Unit) + } + } +} + +sealed interface MarketTestUiState { + object Loading : MarketTestUiState + + data class Success(val data: MarketTestScreenViewState) : MarketTestUiState + + data class Error(val exception: Throwable) : MarketTestUiState +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/dto/MarketPairsUpdateState.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/dto/MarketPairsUpdateState.kt new file mode 100644 index 00000000..630e71c0 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/markettest/dto/MarketPairsUpdateState.kt @@ -0,0 +1,3 @@ +package com.aneonex.bitcoinchecker.tester.ui.features.markettest.dto + +data class MarketPairsUpdateState(val isInProgress: Boolean = false, val error: String? = null) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/syncpairs/SyncPairsDialog.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/syncpairs/SyncPairsDialog.kt new file mode 100644 index 00000000..f3518d34 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/features/syncpairs/SyncPairsDialog.kt @@ -0,0 +1,78 @@ +package com.aneonex.bitcoinchecker.tester.ui.features.syncpairs + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.aneonex.bitcoinchecker.datamodule.util.FormatUtilsBase +import com.aneonex.bitcoinchecker.tester.R +import com.aneonex.bitcoinchecker.tester.domain.model.MyMarketPairsInfo +import com.aneonex.bitcoinchecker.tester.ui.features.markettest.dto.MarketPairsUpdateState + +@Composable +fun SyncPairsDialog( + currencyPairsInfo: MyMarketPairsInfo, + resultState: MarketPairsUpdateState, + onDismiss: () -> Unit, + onSyncClick: () -> Unit +) { + AlertDialog( +// modifier = Modifier.fillMaxWidth(), + onDismissRequest = { +// openDialog.value = false + onDismiss() + }, + title = { + Column { + Text(text = stringResource(R.string.checker_add_dynamic_currency_pairs_dialog_title)) + Text( + text = stringResource(R.string.checker_add_check_currency_empty_warning_summary), + style = MaterialTheme.typography.bodyMedium + ) + } + }, + text = { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button( + modifier = Modifier.fillMaxWidth(), + enabled = !resultState.isInProgress, + onClick = { + onSyncClick() + }) { + Text(stringResource(R.string.checker_add_dynamic_currency_pairs_dialog_synchronize)) + } + + val lastSyncDateText = + if(currencyPairsInfo.lastSyncDate > 0) + FormatUtilsBase.formatSameDayTimeOrDate(LocalContext.current, currencyPairsInfo.lastSyncDate) + else stringResource(id = R.string.checker_add_dynamic_currency_pairs_dialog_last_sync_never) + + Text( + text = stringResource(id = R.string.checker_add_dynamic_currency_pairs_dialog_last_sync, lastSyncDateText) + ) + Text("Currency pairs: ${currencyPairsInfo.size}") + resultState.error?.also { + Text(stringResource(R.string.check_error_generic_prefix, it)) + } + } + }, + confirmButton = { + Button( +// modifier = Modifier.fillMaxWidth(), + onClick = { +// openDialog.value = false + onDismiss() + } + ) { + Text(stringResource(android.R.string.ok)) + } + } + ) +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/navigation/MyAppNavHost.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/navigation/MyAppNavHost.kt new file mode 100644 index 00000000..63345424 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/navigation/MyAppNavHost.kt @@ -0,0 +1,32 @@ +package com.aneonex.bitcoinchecker.tester.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.aneonex.bitcoinchecker.tester.ui.features.markettest.MarketTestScreen + +@Composable +fun MyAppNavHost() { + val navigation = rememberNavController() + + NavHost( + navController = navigation, + startDestination = ScreenRoute.MarketTest + ) { +/* + composable( + route = Screen.Splash.route + ) { + SplashScreen( + navigationController = navigation + ) + } +*/ + composable( + route = ScreenRoute.MarketTest + ) { + MarketTestScreen() + } + } +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/navigation/ScreenRoute.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/navigation/ScreenRoute.kt new file mode 100644 index 00000000..53bffe52 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/navigation/ScreenRoute.kt @@ -0,0 +1,6 @@ +package com.aneonex.bitcoinchecker.tester.ui.navigation + +object ScreenRoute { + const val MarketTest = "market_test" +// object Market : Screen("market") +} diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Color.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Color.kt new file mode 100644 index 00000000..384cba50 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Color.kt @@ -0,0 +1,20 @@ +package com.aneonex.bitcoinchecker.tester.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) +val Teal700 = Color(0xFF018786) +val Green500 = Color(0xFF26DE9B) +val Green500Transparent80 = Color(0x8026DE9B) +val Red500 = Color(0xFFEB6365) +val Red500Transparent80 = Color(0x80EB6365) +val Gray200 = Color(0xFFCCD0DD) +val Gray700 = Color(0xFF646C81) +val Gray900 = Color(0xFF23262D) +val Gray950 = Color(0xFFCCD0DD) +val Black = Color.Black //Color(0xFF000000) +val BlackTransparent20 = Color(0x07000000) +val White = Color.White //Color(0xFFFFFFFF) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Shape.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Shape.kt new file mode 100644 index 00000000..b69674f8 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.aneonex.bitcoinchecker.tester.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val AppShapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Theme.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Theme.kt new file mode 100644 index 00000000..d5ce992a --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.aneonex.bitcoinchecker.tester.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val LightColorScheme = lightColorScheme( + primary = Purple500, +// primaryVariant = Purple700, +// secondary = Teal200 + +/* + primary = black, + primaryVariant = black, + onPrimary = white, + secondary = teal200, + secondaryVariant = teal700, + onSecondary = black, + surface = white, + onSurface = black, + background = white, + onBackground = black, +*/ +) + +private val DarkColorScheme = darkColorScheme( + primary = Purple200, +// primaryVariant = Purple700, + secondary = Teal200 + +/* + primary = white, + primaryVariant = white, + onPrimary = black, + secondary = teal200, + secondaryVariant = teal200, + onSecondary = white, + surface = black, + onSurface = white, + background = black, + onBackground = white, + */ +) + +@Composable +fun MyAppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + MaterialTheme( + colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme, + typography = Typography, + shapes = AppShapes, + content = content + ) +} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Typography.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Typography.kt new file mode 100644 index 00000000..6734de42 --- /dev/null +++ b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/ui/theme/Typography.kt @@ -0,0 +1,25 @@ +package com.aneonex.bitcoinchecker.tester.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + + +val Typography = Typography( +// defaultFontFamily = redHatDisplay, +/* + h1 = TextStyle(fontSize = 64.sp, fontWeight = FontWeight.Black), + h2 = TextStyle(fontSize = 48.sp, fontWeight = FontWeight.Black), + h3 = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), + h4 = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold), + h5 = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold), + h6 = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Bold), + body1 = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Bold), + body2 = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium), + subtitle1 = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Bold), + subtitle2 = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium), + button = TextStyle(fontWeight = FontWeight.Medium), + overline = TextStyle(fontWeight = FontWeight.Medium), + */ +) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/CheckErrorsUtils.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/CheckErrorsUtils.kt deleted file mode 100644 index b05f0c1e..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/CheckErrorsUtils.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.util - -import android.content.Context -import android.text.SpannableStringBuilder -import com.android.volley.* -import com.aneonex.bitcoinchecker.tester.R -import java.io.PrintWriter -import java.io.StringWriter - -object CheckErrorsUtils { - private const val RAW_RESPONSE_CHARS_LIMIT = 5000 - - fun parseVolleyErrorMsg(context: Context, error: VolleyError?): String { - return when (error) { - is NetworkError -> context.getString(R.string.check_error_network) - is TimeoutError -> context.getString(R.string.check_error_timeout) - is ClientError -> context.getString(R.string.check_error_client) - is ServerError -> context.getString(R.string.check_error_server) - is ParseError -> context.getString(R.string.check_error_parse) - else -> context.getString(R.string.check_error_unknown) - } - } - -/* - fun formatError(context: Context, errorMsg: String?): String { - return context.getString(R.string.check_error_generic_prefix, errorMsg ?: "UNKNOWN") - } -*/ - - private fun formatMapToHtmlString(headers: Map?): String { - var output = "" - if(headers != null) { - for ((key, value) in headers) { - output += String.format("%1\$s = %2\$s", key, value) - } - } - return output - } - - fun formatResponseDebug(context: Context, ssb: SpannableStringBuilder, url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, rawResponse: String?, exception: Exception?): SpannableStringBuilder { - if (url != null) { - ssb.append("\n\n") - ssb.append(context.getString(R.string.ticker_raw_url, url)) - } - if (requestHeaders != null) { - ssb.append("\n\n") - ssb.append(SpannableUtils.fromHtml(context.getString(R.string.ticker_raw_request_headers) + "" + formatMapToHtmlString(requestHeaders) + "")) - } - if (networkResponse != null) { - ssb.append("\n\n") - ssb.append(context.getString(R.string.ticker_raw_response_code, networkResponse.statusCode.toString())) - ssb.append("\n\n") - ssb.append(SpannableUtils.fromHtml(context.getString(R.string.ticker_raw_response_headers) + "" + formatMapToHtmlString(networkResponse.headers) + "")) - } - if (rawResponse != null) { - ssb.append("\n\n") - var limitedRawResponse: String = rawResponse - if (rawResponse.length > RAW_RESPONSE_CHARS_LIMIT) { - limitedRawResponse = rawResponse.substring(0, RAW_RESPONSE_CHARS_LIMIT) + "..." - } - ssb.append(SpannableUtils.fromHtml(context.getString(R.string.ticker_raw_response) + "" + limitedRawResponse + "")) - } - if (exception != null) { - ssb.append("\n\n") - ssb.append(SpannableUtils.fromHtml(context.getString(R.string.ticker_raw_stacktrace) + "" + printException(exception) + "")) - } - return ssb - } - - private fun printException(e: Exception): String { - val errors = StringWriter() - e.printStackTrace(PrintWriter(errors)) - return errors.toString() - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/HttpsHelper.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/HttpsHelper.kt deleted file mode 100644 index 7d6661ea..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/HttpsHelper.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.util - -import android.content.Context -import com.android.volley.RequestQueue -import com.android.volley.toolbox.HurlStack -import com.android.volley.toolbox.Volley -import java.security.cert.CertificateException -import java.security.cert.X509Certificate -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLSocketFactory -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager - -object HttpsHelper { - fun newRequestQueue(context: Context?): RequestQueue { - return Volley.newRequestQueue(context, HurlStack(null, mySSLSocketFactory)) - } - - private val mySSLSocketFactory: SSLSocketFactory? - get() = try { - val sslContext = SSLContext.getInstance("TLS") - val tm: TrustManager = object : X509TrustManager { - @Throws(CertificateException::class) - override fun checkClientTrusted( - chain: Array, - authType: String) { - } - - @Throws(CertificateException::class) - override fun checkServerTrusted( - chain: Array, - authType: String) { - } - - override fun getAcceptedIssuers(): Array? { - return null - } - } - sslContext.init(null, arrayOf(tm), null) - sslContext.socketFactory - } catch (e: Exception) { - e.printStackTrace() - null - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/MarketCurrencyPairsStore.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/MarketCurrencyPairsStore.kt deleted file mode 100644 index 74300668..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/MarketCurrencyPairsStore.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.util - -import android.content.Context -import android.content.SharedPreferences -import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairsListWithDate -import com.google.gson.Gson - -object MarketCurrencyPairsStore { - private fun getSharedPreferences(context: Context): SharedPreferences { - return context.applicationContext.getSharedPreferences("MARKET_CURRENCY_PAIRS", Context.MODE_PRIVATE) - } - - fun savePairsForMarket(context: Context, marketKey: String, currencyPairsListWithDate: CurrencyPairsListWithDate?) { - try { - savePairsStringForMarket(context, marketKey, Gson().toJson(currencyPairsListWithDate)) - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun savePairsStringForMarket(context: Context, marketKey: String, jsonString: String) { - getSharedPreferences(context).edit().putString(marketKey, jsonString).apply() - } - - fun getPairsForMarket(context: Context, marketKey: String): CurrencyPairsListWithDate? { - val pairsJson = getPairsStringForMarket(context, marketKey) ?: return null - - return try { - Gson().fromJson(pairsJson, CurrencyPairsListWithDate::class.java) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - private fun getPairsStringForMarket(context: Context, marketKey: String): String? { - return getSharedPreferences(context).getString(marketKey, null) - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/SpannableUtils.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/SpannableUtils.kt deleted file mode 100644 index ce6293f3..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/util/SpannableUtils.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.util - -import android.text.Spanned -import androidx.core.text.HtmlCompat - -object SpannableUtils { - fun fromHtml(source: String): Spanned { - return HtmlCompat.fromHtml(source, HtmlCompat.FROM_HTML_MODE_COMPACT) - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerErrorParsedError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerErrorParsedError.kt deleted file mode 100644 index d575ad77..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerErrorParsedError.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley - -import com.android.volley.ParseError - -class CheckerErrorParsedError(val errorMsg: String?) : ParseError() { - - companion object { - private const val serialVersionUID = -8541129282633613311L - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerVolleyMainRequest.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerVolleyMainRequest.kt deleted file mode 100644 index b89d5f4e..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerVolleyMainRequest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley - -import android.text.TextUtils -import com.android.volley.DefaultRetryPolicy -import com.android.volley.Response -import com.android.volley.toolbox.RequestFuture -import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo -import com.aneonex.bitcoinchecker.datamodule.model.Market -import com.aneonex.bitcoinchecker.datamodule.model.Ticker -import com.aneonex.bitcoinchecker.tester.volley.CheckerVolleyMainRequest.TickerWrapper -import com.aneonex.bitcoinchecker.tester.volley.generic.GenericCheckerVolleyRequest - -class CheckerVolleyMainRequest(market: Market, checkerInfo: CheckerInfo, listener: Response.Listener, errorListener: Response.ErrorListener) - : GenericCheckerVolleyRequest(market.getUrl(0, checkerInfo), market.getPostRequestInfo(0, checkerInfo), checkerInfo, listener, errorListener) { - - private val market: Market - @Throws(Exception::class) - override fun parseNetworkResponse(headers: Map?, responseString: String): TickerWrapper { - val tickerWrapper = TickerWrapper() - var tickerParseException: Exception? = null - try { - tickerWrapper.ticker = market.parseTickerMain(0, - responseString, TickerImpl(), checkerInfo) - } catch (e: Exception) { - e.printStackTrace() - tickerParseException = e - tickerWrapper.ticker = null - } - if (tickerWrapper.ticker == null || tickerWrapper.ticker!!.last <= Ticker.NO_DATA) { - val errorMsg: String? = try { - market.parseErrorMain(0, responseString, checkerInfo) - } catch (e: Exception) { - tickerParseException?.message - } - throw CheckerErrorParsedError(errorMsg) - } - - val numOfRequests = market.getNumOfRequests(checkerInfo) - if (numOfRequests > 1) { - for (requestId in 1 until numOfRequests) { - try { - val future = RequestFuture.newFuture() - val nextUrl = market.getUrl(requestId, checkerInfo) - val nextPostRequestInfo = market.getPostRequestInfo(requestId, checkerInfo) - if (!TextUtils.isEmpty(nextUrl)) { - val request = CheckerVolleyNextRequest(nextUrl, nextPostRequestInfo, checkerInfo, future) - requestQueue!!.add(request) - val nextResponse = future.get() // this will block - market.parseTickerMain(requestId, nextResponse, tickerWrapper.ticker!!, checkerInfo) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - return tickerWrapper - } - - inner class TickerWrapper { - var ticker: Ticker? = null - } - - init { - retryPolicy = DefaultRetryPolicy(5000, 3, 1.5f) - this.market = market - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerVolleyNextRequest.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerVolleyNextRequest.kt deleted file mode 100644 index fd1f40a0..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/CheckerVolleyNextRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley - -import com.android.volley.toolbox.RequestFuture -import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo -import com.aneonex.bitcoinchecker.datamodule.model.PostRequestInfo -import com.aneonex.bitcoinchecker.tester.volley.generic.GenericCheckerVolleyRequest - -class CheckerVolleyNextRequest(url: String?, postRequestInfo: PostRequestInfo?, checkerInfo: CheckerInfo, future: RequestFuture) - : GenericCheckerVolleyRequest(url, postRequestInfo, checkerInfo, future, future) { - @Throws(Exception::class) - override fun parseNetworkResponse(headers: Map?, responseString: String): String { - return responseString - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/DynamicCurrencyPairsVolleyMainRequest.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/DynamicCurrencyPairsVolleyMainRequest.kt deleted file mode 100644 index 8c07fcb6..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/DynamicCurrencyPairsVolleyMainRequest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley - -import android.content.Context -import android.text.TextUtils -import com.android.volley.Response -import com.android.volley.toolbox.RequestFuture -import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo -import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairsListWithDate -import com.aneonex.bitcoinchecker.datamodule.model.Market -import com.aneonex.bitcoinchecker.datamodule.util.CurrencyPairsMapHelper -import com.aneonex.bitcoinchecker.tester.util.MarketCurrencyPairsStore -import com.aneonex.bitcoinchecker.tester.volley.generic.GzipVolleyRequest -import java.util.* - -class DynamicCurrencyPairsVolleyMainRequest( - private val context: Context, - private val market: Market, - listener: Response.Listener, - errorListener: Response.ErrorListener) - : GzipVolleyRequest( - market.getCurrencyPairsUrl(0), - market.getCurrencyPairsPostRequestInfo(0), - listener, - errorListener) { - - @Throws(Exception::class) - override fun parseNetworkResponse(headers: Map?, responseString: String): CurrencyPairsMapHelper { - if (isCanceled) - return CurrencyPairsMapHelper(CurrencyPairsListWithDate()) - - val pairs: MutableList = ArrayList() - market.parseCurrencyPairsMain(0, responseString, pairs) - - val numOfRequests = market.currencyPairsNumOfRequests - if (numOfRequests > 1) { - val nextPairs: MutableList = ArrayList() - for (requestId in 1 until numOfRequests) { - try { - val future = RequestFuture.newFuture() - val nextUrl = market.getCurrencyPairsUrl(requestId) - val nextRequestInfo = market.getCurrencyPairsPostRequestInfo(requestId) - - if (!TextUtils.isEmpty(nextUrl)) { - val request = DynamicCurrencyPairsVolleyNextRequest(nextUrl, nextRequestInfo, future) - requestQueue!!.add(request) - val nextResponse = future.get() // this will block - nextPairs.clear() - market.parseCurrencyPairsMain(requestId, nextResponse, nextPairs) - pairs.addAll(nextPairs) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - pairs.sort() - val currencyPairsListWithDate = CurrencyPairsListWithDate().also { - it.date = System.currentTimeMillis() - it.pairs = pairs - } - - if (pairs.size > 0) - MarketCurrencyPairsStore.savePairsForMarket(context, market.key, currencyPairsListWithDate) - - return CurrencyPairsMapHelper(currencyPairsListWithDate) - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/DynamicCurrencyPairsVolleyNextRequest.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/DynamicCurrencyPairsVolleyNextRequest.kt deleted file mode 100644 index 586e715a..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/DynamicCurrencyPairsVolleyNextRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley - -import com.android.volley.toolbox.RequestFuture -import com.aneonex.bitcoinchecker.datamodule.model.PostRequestInfo -import com.aneonex.bitcoinchecker.tester.volley.generic.GzipVolleyRequest - -class DynamicCurrencyPairsVolleyNextRequest(url: String?, postRequestInfo: PostRequestInfo?, future: RequestFuture) : GzipVolleyRequest(url, postRequestInfo, future, future) { - @Throws(Exception::class) - override fun parseNetworkResponse(headers: Map?, responseString: String): String { - return responseString - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/TickerImpl.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/TickerImpl.kt deleted file mode 100644 index a50a3f25..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/TickerImpl.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley - -import com.aneonex.bitcoinchecker.datamodule.model.Ticker -import com.aneonex.bitcoinchecker.datamodule.model.Ticker.Companion.NO_DATA - -internal class TickerImpl: Ticker { - override var bid: Double = NO_DATA.toDouble() - override var ask: Double = NO_DATA.toDouble() - override var vol: Double = NO_DATA.toDouble() - override var volQuote: Double = NO_DATA.toDouble() - override var high: Double = NO_DATA.toDouble() - override var low: Double = NO_DATA.toDouble() - override var last: Double = NO_DATA.toDouble() - override var timestamp: Long = NO_DATA.toLong() -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/UnknownVolleyError.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/UnknownVolleyError.kt deleted file mode 100644 index 505d4e72..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/UnknownVolleyError.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley - -import com.android.volley.VolleyError - -class UnknownVolleyError(cause: Throwable?) : VolleyError(cause) { - companion object { - private const val serialVersionUID = -8541129282633613311L - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/GenericCheckerVolleyRequest.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/GenericCheckerVolleyRequest.kt deleted file mode 100644 index 26afe781..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/GenericCheckerVolleyRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley.generic - -import com.android.volley.Response -import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo -import com.aneonex.bitcoinchecker.datamodule.model.PostRequestInfo - -abstract class GenericCheckerVolleyRequest(url: String?, postRequestInfo: PostRequestInfo?, protected val checkerInfo: CheckerInfo, listener: Response.Listener, errorListener: Response.ErrorListener) - : GzipVolleyRequest(url, postRequestInfo, listener, errorListener) \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/GzipVolleyRequest.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/GzipVolleyRequest.kt deleted file mode 100644 index 7e6d8ead..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/GzipVolleyRequest.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley.generic - -import com.android.volley.* -import com.android.volley.toolbox.HttpHeaderParser -import com.aneonex.bitcoinchecker.datamodule.model.PostRequestInfo -import com.aneonex.bitcoinchecker.tester.volley.CheckerErrorParsedError -import com.aneonex.bitcoinchecker.tester.volley.UnknownVolleyError -import java.io.BufferedReader -import java.io.ByteArrayInputStream -import java.io.InputStreamReader -import java.net.HttpURLConnection -import java.util.zip.GZIPInputStream - -abstract class GzipVolleyRequest(url: String?, private val postRequestInfo: PostRequestInfo?, private val listener: Response.Listener, errorListener: Response.ErrorListener) - : Request(if (postRequestInfo == null) Method.GET else Method.POST, url, errorListener) { - - companion object { - private const val MAX_REDIRECTION_COUNT = 3 - - /** Default charset for JSON request. */ - private val PROTOCOL_CHARSET = Charsets.UTF_8 - } - - private val initialErrorListener = errorListener - private val headers: Map - var requestQueue: RequestQueue? = null - private set - private var redirectionUrl: String? = null - private var redirectionCount = 0 - private var requestHeaders: Map? = null - private var networkResponse: NetworkResponse? = null - private var responseString: String? = null - - init { - if(method == Method.POST && postRequestInfo == null) throw IllegalArgumentException("Invalid arguments: postRequestInfo cannot be null for POST method") - - headers = mutableMapOf ( - "Accept-Encoding" to "gzip", - "User-Agent" to "Bitcoin Checker (gzip)" - ) - - postRequestInfo?.headers?.let { - for (item in it){ - headers[item.key] = item.value - } - } - - // Disable cache for volley requests - setShouldCache(false) - } - - override fun getUrl(): String { - return if (redirectionUrl != null) redirectionUrl.toString() else super.getUrl() - } - - override fun getBodyContentType(): String { - return if(postRequestInfo != null) "application/json; charset=$PROTOCOL_CHARSET" else super.getBodyContentType() - } - - override fun getBody(): ByteArray { - return if(postRequestInfo != null) return postRequestInfo.body.toByteArray(PROTOCOL_CHARSET) else super.getBody() - } - - @Throws(AuthFailureError::class) - override fun getHeaders(): Map { - requestHeaders = headers //: super.getHeaders() - return requestHeaders!! - } - - override fun setRequestQueue(requestQueue: RequestQueue): Request<*> { - this.requestQueue = requestQueue - return super.setRequestQueue(requestQueue) - } - - override fun deliverError(error: VolleyError) { - if (error.networkResponse != null) { - val statusCode = error.networkResponse.statusCode - if (statusCode == HttpURLConnection.HTTP_MOVED_PERM || statusCode == HttpURLConnection.HTTP_MOVED_TEMP) { - val location = error.networkResponse.headers?.get("Location") - if (location != null && redirectionCount < MAX_REDIRECTION_COUNT) { - ++redirectionCount - redirectionUrl = location - requestQueue!!.add(this) - return - } - } - } - if (initialErrorListener is ResponseErrorListener) - initialErrorListener.onErrorResponse(url, requestHeaders, networkResponse, responseString, error) - else super.deliverError(error) - } - - override fun deliverResponse(response: T) { - if (listener is ResponseListener<*>) (listener as ResponseListener).onResponse(url, requestHeaders, networkResponse, responseString, response) else listener.onResponse(response) - } - - @Throws(Exception::class) - protected abstract fun parseNetworkResponse(headers: Map?, responseString: String): T? - - override fun parseNetworkResponse(response: NetworkResponse): Response { -// var response: NetworkResponse? = response - return try { - networkResponse = response - val encoding = response.headers?.get("Content-Encoding") - val responseString = if (encoding != null && encoding.contains("gzip")) { - decodeGZip(response.data) - } else { - String(response.data, charset(HttpHeaderParser.parseCharset(response.headers))) - } - this.responseString = responseString - val headers = response.headers - val cacheHeaders = HttpHeaderParser.parseCacheHeaders(response) -// response = null - Response.success(parseNetworkResponse(headers, responseString), cacheHeaders) - } catch (checkerErrorParsedError: CheckerErrorParsedError) { - Response.error(checkerErrorParsedError) - } catch (e: Exception) { - Response.error(ParseError(e)) - } catch (e: Throwable) { - Response.error(UnknownVolleyError(e)) - } - } - - @Throws(Exception::class) - private fun decodeGZip(data: ByteArray): String { - var responseString = "" - var bais: ByteArrayInputStream? = null - var gzis: GZIPInputStream? = null - var reader: InputStreamReader? = null - var bufReader: BufferedReader? = null - try { - bais = ByteArrayInputStream(data) - gzis = GZIPInputStream(bais) - reader = InputStreamReader(gzis) - bufReader = BufferedReader(reader) - var readed: String? - while (bufReader.readLine().also { readed = it } != null) { - responseString += """ - $readed - - """.trimIndent() - } - } catch (e: Exception) { - throw e - } finally { - try { - bais?.close() - gzis?.close() - reader?.close() - bufReader?.close() - } catch (e: Exception) { - } - } - return responseString - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/ResponseErrorListener.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/ResponseErrorListener.kt deleted file mode 100644 index 9786fa30..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/ResponseErrorListener.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley.generic - -import com.android.volley.NetworkResponse -import com.android.volley.Response -import com.android.volley.VolleyError - -abstract class ResponseErrorListener : Response.ErrorListener { - abstract fun onErrorResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, error: VolleyError) - override fun onErrorResponse(error: VolleyError) { - onErrorResponse(null, null, null, null, error) - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/ResponseListener.kt b/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/ResponseListener.kt deleted file mode 100644 index fd2440b5..00000000 --- a/dataModuleTester/src/main/java/com/aneonex/bitcoinchecker/tester/volley/generic/ResponseListener.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.aneonex.bitcoinchecker.tester.volley.generic - -import com.android.volley.NetworkResponse -import com.android.volley.Response - -abstract class ResponseListener : Response.Listener { - abstract fun onResponse(url: String?, requestHeaders: Map?, networkResponse: NetworkResponse?, responseString: String?, response: T) - override fun onResponse(response: T) { - onResponse(null, null, null, null, response) - } -} \ No newline at end of file diff --git a/dataModuleTester/src/main/res/drawable-hdpi/ic_action_info.png b/dataModuleTester/src/main/res/drawable-hdpi/ic_action_info.png deleted file mode 100644 index f8b5c0fa6df00af51a7e6aebf29ab365546ea55c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 877 zcmV-z1CsoSP)_EX4 zXj~|ON6!bkH})xUc+w6C4L!MTrz8 zFd75)fT=Bp{NVEo@CJNxzYm}v21bHjEn{!_$W(ZC>tbGE~%znkZe=)A)o{M z5Xk4X@=J4!m#a;3Ry?53a*ZDRe-11LIujFt>0tX1xP?$f8~OYMkmojg8MECH*hjx3 zXG!HwkHB!C=rql;1y%wP!tOeOGwCe{_6p=2Naq>`i*RpJaSpH`x>w80E|4-^FCMqOExECjY9KpFQrP$aT8g~B*A%6Nrf zYf}Uo1(cx=6eVy70h_>OFyCqclIxYfU-`32>`Fx* zwq_$lg<9JlwptHX+SB4o2E7JZPm4O(cR*X77L(B-;)hy3?k0((J+FUe;5+coKCdT< zblZ2|JkV{1Srs(|tSeR3xMl~cT3M^yW(TY*Rn@pZAUMe}+kh<+00000NkvXXu0mjf DWH*W> diff --git a/dataModuleTester/src/main/res/drawable-hdpi/ic_launcher.png b/dataModuleTester/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index bf49573412c3554bb7470bad6156408cd952d89d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3134 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 diff --git a/dataModuleTester/src/main/res/drawable-mdpi/ic_action_info.png b/dataModuleTester/src/main/res/drawable-mdpi/ic_action_info.png deleted file mode 100644 index b86582beca244ea798452c6928631685688244a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 514 zcmV+d0{#7oP)A;sy$0qlJY_ zp@kr#g?z+{AE3lpD75)TMyI|I~`D?V(6aq6mH|j zR2&Z!V|)yzpv5rICFQHI4j(RJTm`gaYy+C%NaYVn@6;f*q`fc--EgJyXEqTo0{Sts z1dz%26`2k#m2zj#2NYz)8=Aa4s!03r60kY=jPzIeLgG=8@ zHK45fS3q19|HtqOZ4eq0Q359DB!u67bW0=)^E@g z){!#lJEzix5D(~X611hz8b^TabD7aBEExopP(H8fM8LbD-3;Z6(D2J+KTS;@7^cN0 zbis+ia5?2Ks!k$a=Y!29ek2w~Kz7rg&m=$&n`Nkm$CLsj7$0C7R`N!G_a$%%Lf05b z6oj%=wr%JtAkj7y0k(t4ML?>9$aWMLA#zYf^)~5eAi`50#bPh%Wh8^|L8{A0Psrx=zA(&oHkpKVy07*qoM6N<$ Eg2Id4IRF3v diff --git a/dataModuleTester/src/main/res/drawable-mdpi/ic_launcher.png b/dataModuleTester/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index c205afeb2b8fe654977314edb553975922de0c8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2156 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 diff --git a/dataModuleTester/src/main/res/drawable-xhdpi/ic_launcher.png b/dataModuleTester/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 4a3ea3a8d095b383b449a3f79bae33777528a7c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3964 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 diff --git a/dataModuleTester/src/main/res/drawable-xxhdpi/ic_action_info.png b/dataModuleTester/src/main/res/drawable-xxhdpi/ic_action_info.png deleted file mode 100644 index 74bdb8f5587f8978769eb6643fb0a5630c29d338..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1889 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 diff --git a/dataModuleTester/src/main/res/drawable-xxxhdpi/ic_launcher.png b/dataModuleTester/src/main/res/drawable-xxxhdpi/ic_launcher.png deleted file mode 100644 index 1146d5c0cd5b6501d89af6b7cbff5e563c0f9d8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7675 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 F`X8^cjUoU5 diff --git a/dataModuleTester/src/main/res/drawable/ic_error.xml b/dataModuleTester/src/main/res/drawable/ic_error.xml new file mode 100644 index 00000000..659520c1 --- /dev/null +++ b/dataModuleTester/src/main/res/drawable/ic_error.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dataModuleTester/src/main/res/drawable/ic_launcher_foreground.xml b/dataModuleTester/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..ead8b8e3 --- /dev/null +++ b/dataModuleTester/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/dataModuleTester/src/main/res/drawable/ic_no_connection.xml b/dataModuleTester/src/main/res/drawable/ic_no_connection.xml new file mode 100644 index 00000000..5f205368 --- /dev/null +++ b/dataModuleTester/src/main/res/drawable/ic_no_connection.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dataModuleTester/src/main/res/layout/dynamic_currency_pairs_dialog.xml b/dataModuleTester/src/main/res/layout/dynamic_currency_pairs_dialog.xml deleted file mode 100644 index f5e44c07..00000000 --- a/dataModuleTester/src/main/res/layout/dynamic_currency_pairs_dialog.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - -