From cab4440d8d58bcb5f5edc1c4779b951f97ad8997 Mon Sep 17 00:00:00 2001 From: Dmitriy Chernysh Date: Fri, 3 Apr 2020 21:42:57 +0300 Subject: [PATCH] Added country details screen #5 (#24) * feature: added statistic parser and statistic API * configured koin * added repository for statistic * added jsoup to gradle dependencies * added models for statistic from API and from html parser * renamed total models and repository, interactor * added statistic api [SO-5] * fix: fixed merge conflicts * fixed merge conflicts [SO-5] * feature: added database implementation for statistics * refactored module DI * implemented logic for saving data [SO-5] * feature: added database implementation for statistics * added ID [SO-5] * feature: added database implementation for statistics * created two primary keys [SO-5] * feature: added database implementation for statistics * changed primary key to province [SO-5] * feature: added database implementation for statistics * added inject StatisticCovidCache to Module.kt [SO-5] * Fixes * feature: created implementation getting logic from database to viewmodel * renamed Country.kt to TotalCountry.kt * created model StatisticCountry.kt * added mapping for StatisticCountry.kt [SO-5] * feature: created statistic for country * created viewmodel * created fragment * create views for fragment, recycler item * create DiffUtil callback [SO-5] * feature: added throwable for parser * created throwable for parser * implemented logic for mapping * added error to strings.xml * added icon for StatisticCountryFragment.kt [SO-5] * fix: fixed data binding and parser * fixed data format value in mapper for xml * fixed parser, changed urls and remove const count countries * remove deprecated databinding execute method and added recommended by IDE [CO-5] * feature: added country details country statistic * added click listener to CountriesListAdapter.kt * added inject logic to DI * added safe args to gradle * added show country statistic method to NavigationExtensions.kt [CO-5] * feature: fetch confirmed-deaths-recovered in one model * added to DayStatistic three confirmed-deaths-recovered [SO-5] * fix: fixed statistic data calculating * fixed fetching statistic data * fixed calculating statistic by day [CO-5] * feature: added collapsed view for statistic screen * added draggable icon * configured fragment_statistic_country.xml * changed colorPrimaryLight * added BottomSheetBehaviour init method to StatisticCountryFragment.kt [SO-5] * feature: added chart * added MPAChart lib * added RxRelay lib * added temp chart to fragment * added time format method to DateExtensions.kt * added behavior relay to StatisticCountryViewModel.kt [SO-5] * feature: added ChartView * incremented chart lib * created custom view chart * added map-method for float to FormatExtensions.kt [SO-5] * Fixed statistics list and refactoring * Merge the latest Taras's changes * feature: changed time impl * changed time format from string to long in data layers * created view for total data * removed unusable methods form cache layer [SO-5] * feature: added total to statistic * added total * added total string * created total view model * renamed chart view model * fixed url for parser [SO-5] * UI minor changes * Show a date in total values under the chart * Increment a database version * Changed FAB behaviour Co-authored-by: KoiDev --- app/build.gradle | 12 +- app/src/main/AndroidManifest.xml | 3 +- .../java/com/mobiledevpro/app/common/App.kt | 13 +- .../java/com/mobiledevpro/app/di/Module.kt | 95 ++++++- .../app/ui/countries/CountriesListFragment.kt | 37 ++- .../countries/adapter/CountriesListAdapter.kt | 23 +- .../mobiledevpro/app/ui/main/MainActivity.kt | 22 +- .../app/ui/main/viemodel/MainViewModel.kt | 14 + .../ui/statistic/StatisticCountryFragment.kt | 87 +++++++ .../adapter/StatisticCountryListAdapter.kt | 63 +++++ .../viewmodel/StatisticCountryViewModel.kt | 133 ++++++++++ .../app/ui/total/TotalFragment.kt | 9 +- .../app/ui/total/viewmodel/TotalViewModel.kt | 45 ++-- .../app/ui/widget/StatisticLineChart.kt | 148 +++++++++++ .../mobiledevpro/app/utils/DateExtensions.kt | 8 +- .../app/utils/FormatExtensions.kt | 16 +- .../app/utils/NavigationExtensions.kt | 28 +- .../app/utils/diff/CountiesDiffUtil.kt | 6 +- .../utils/diff/StatisticCountryDiffUtil.kt | 20 ++ .../utils/provider/DefaultResourceProvider.kt | 7 +- app/src/main/res/drawable/ic_drag_handle.xml | 5 + .../layouts/activity/layout/activity_main.xml | 3 +- .../activity/layout/activity_splash.xml | 7 +- .../layouts/adapter/layout/item_country.xml | 2 +- .../adapter/layout/item_statistic_country.xml | 68 +++++ .../layout/fragment_countries_list.xml | 1 + .../layout/fragment_statistic_country.xml | 127 +++++++++ .../fragment/layout/fragment_total.xml | 8 +- app/src/main/res/navigation/navigation.xml | 25 +- app/src/main/res/values-it/strings.xml | 8 +- app/src/main/res/values-ru/strings.xml | 8 +- app/src/main/res/values-uk/strings.xml | 6 +- app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 6 +- build.gradle | 22 +- data-local/build.gradle | 2 +- .../2.json | 115 ++++++++- .../3.json | 244 ++++++++++++++++++ .../local/database/AppDatabase.kt | 33 ++- .../local/database/DefaultCovidCache.kt | 63 ----- .../local/database/dao/CountryDataDao.kt | 16 -- .../statistic/DefaultStatisticsCovidCache.kt | 44 ++++ .../statistic/dao/StatisticCountryDataDao.kt | 20 ++ .../dao/StatisticDayCountryDataDao.kt | 14 + .../model/CachedDayTotalCountryStatistic.kt | 25 ++ .../statistic/model/CachedStatisticCountry.kt | 23 ++ ...achedStatisticCountryWithDailyStatistic.kt | 16 ++ .../database/total/DefaultTotalCovidCache.kt | 63 +++++ .../local/database/{ => total}/dao/BaseDao.kt | 4 +- .../database/total/dao/TotalCountryDataDao.kt | 16 ++ .../database/{ => total}/dao/TotalDataDao.kt | 7 +- .../database/{ => total}/model/CachedTotal.kt | 2 +- .../model/CachedTotalCounties.kt} | 6 +- .../local/mapper/MapperExtension.kt | 43 ++- data-remote/build.gradle | 3 + .../DefaultStatisticCovidRemote.kt | 19 ++ .../DefaultStatisticsParserHtml.kt | 134 ++++++++++ ...aIRemote.kt => DefaultTotalCovidRemote.kt} | 21 +- .../remote/mapper/MapperExtension.kt | 29 ++- ...sResponse.kt => CountriesTotalResponse.kt} | 10 +- .../model/response/StatisticResponse.kt | 20 ++ .../remote/model/response/TotalResponse.kt | 6 +- .../remote/service/RemoteServiceFactory.kt | 22 +- .../service/api/FullStatisticRestApiClient.kt | 25 ++ ...RestApiClient.kt => TotalRestApiClient.kt} | 6 +- .../interceptor/ApiRequestInterceptor.kt | 16 -- .../ApiStatisticsRequestInterceptor.kt | 32 +++ .../data/mapper/MapperExtension.kt | 49 ++-- ...CountryEntity.kt => CountryTotalEntity.kt} | 2 +- .../mobiledevpro/data/model/ErrorEntity.kt | 2 + .../model/statistic/CountryStatisticEntity.kt | 10 + .../data/model/statistic/StatisticEntity.kt | 47 ++++ .../repository/parcer/StatisticsParserHtml.kt | 13 + .../DefaultStatisticDataRepository.kt | 130 ++++++++++ .../statistic/StatisticCovidCache.kt | 12 + .../statistic/StatisticCovidRemote.kt | 9 + .../data/repository/userdata/CovidFactory.kt | 8 +- .../userdata/DefaultTotalDataRepository.kt | 34 +-- .../{CovidCache.kt => TotalCovidCache.kt} | 9 +- .../{CovidRemote.kt => TotalCovidRemote.kt} | 6 +- .../com/mobiledevpro/domain/common/Result.kt | 1 + .../com/mobiledevpro/domain/error/Error.kt | 2 + .../domain/extension/MaperExtension.kt | 2 + .../domain/model/StatisticCountry.kt | 36 +++ .../model/{Country.kt => TotalCountry.kt} | 2 +- .../data/DefaultStatisticDataInteractor.kt | 48 ++++ .../statistic/data/StatisticDataInteractor.kt | 17 ++ .../statistic/data/StatisticDataRepository.kt | 16 ++ .../totaldata/DefaultTotalDataInteractor.kt | 4 +- .../domain/totaldata/TotalDataInteractor.kt | 4 +- .../domain/totaldata/TotalDataRepository.kt | 8 +- 91 files changed, 2333 insertions(+), 324 deletions(-) create mode 100644 app/src/main/java/com/mobiledevpro/app/ui/statistic/StatisticCountryFragment.kt create mode 100644 app/src/main/java/com/mobiledevpro/app/ui/statistic/adapter/StatisticCountryListAdapter.kt create mode 100644 app/src/main/java/com/mobiledevpro/app/ui/statistic/viewmodel/StatisticCountryViewModel.kt create mode 100644 app/src/main/java/com/mobiledevpro/app/ui/widget/StatisticLineChart.kt create mode 100644 app/src/main/java/com/mobiledevpro/app/utils/diff/StatisticCountryDiffUtil.kt create mode 100644 app/src/main/res/drawable/ic_drag_handle.xml create mode 100644 app/src/main/res/layouts/adapter/layout/item_statistic_country.xml create mode 100644 app/src/main/res/layouts/fragment/layout/fragment_statistic_country.xml create mode 100644 data-local/schemas/com.mobiledevpro.local.database.AppDatabase/3.json delete mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/DefaultCovidCache.kt delete mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/dao/CountryDataDao.kt create mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/statistic/DefaultStatisticsCovidCache.kt create mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/statistic/dao/StatisticCountryDataDao.kt create mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/statistic/dao/StatisticDayCountryDataDao.kt create mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedDayTotalCountryStatistic.kt create mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedStatisticCountry.kt create mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedStatisticCountryWithDailyStatistic.kt create mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/total/DefaultTotalCovidCache.kt rename data-local/src/main/java/com/mobiledevpro/local/database/{ => total}/dao/BaseDao.kt (87%) create mode 100644 data-local/src/main/java/com/mobiledevpro/local/database/total/dao/TotalCountryDataDao.kt rename data-local/src/main/java/com/mobiledevpro/local/database/{ => total}/dao/TotalDataDao.kt (60%) rename data-local/src/main/java/com/mobiledevpro/local/database/{ => total}/model/CachedTotal.kt (84%) rename data-local/src/main/java/com/mobiledevpro/local/database/{model/CachedCounties.kt => total/model/CachedTotalCounties.kt} (78%) create mode 100644 data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultStatisticCovidRemote.kt create mode 100644 data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultStatisticsParserHtml.kt rename data-remote/src/main/java/com/mobiledevpro/remote/implementation/{DefaultTotalDataIRemote.kt => DefaultTotalCovidRemote.kt} (53%) rename data-remote/src/main/java/com/mobiledevpro/remote/model/response/{CountriesResponse.kt => CountriesTotalResponse.kt} (67%) create mode 100644 data-remote/src/main/java/com/mobiledevpro/remote/model/response/StatisticResponse.kt create mode 100644 data-remote/src/main/java/com/mobiledevpro/remote/service/api/FullStatisticRestApiClient.kt rename data-remote/src/main/java/com/mobiledevpro/remote/service/api/{RestApiClient.kt => TotalRestApiClient.kt} (96%) delete mode 100644 data-remote/src/main/java/com/mobiledevpro/remote/service/interceptor/ApiRequestInterceptor.kt create mode 100644 data-remote/src/main/java/com/mobiledevpro/remote/service/interceptor/ApiStatisticsRequestInterceptor.kt rename data/src/main/java/com/mobiledevpro/data/model/{CountryEntity.kt => CountryTotalEntity.kt} (88%) create mode 100644 data/src/main/java/com/mobiledevpro/data/model/statistic/CountryStatisticEntity.kt create mode 100644 data/src/main/java/com/mobiledevpro/data/model/statistic/StatisticEntity.kt create mode 100644 data/src/main/java/com/mobiledevpro/data/repository/parcer/StatisticsParserHtml.kt create mode 100644 data/src/main/java/com/mobiledevpro/data/repository/statistic/DefaultStatisticDataRepository.kt create mode 100644 data/src/main/java/com/mobiledevpro/data/repository/statistic/StatisticCovidCache.kt create mode 100644 data/src/main/java/com/mobiledevpro/data/repository/statistic/StatisticCovidRemote.kt rename data/src/main/java/com/mobiledevpro/data/repository/userdata/{CovidCache.kt => TotalCovidCache.kt} (62%) rename data/src/main/java/com/mobiledevpro/data/repository/userdata/{CovidRemote.kt => TotalCovidRemote.kt} (67%) create mode 100644 domain/src/main/java/com/mobiledevpro/domain/model/StatisticCountry.kt rename domain/src/main/java/com/mobiledevpro/domain/model/{Country.kt => TotalCountry.kt} (94%) create mode 100644 domain/src/main/java/com/mobiledevpro/domain/statistic/data/DefaultStatisticDataInteractor.kt create mode 100644 domain/src/main/java/com/mobiledevpro/domain/statistic/data/StatisticDataInteractor.kt create mode 100644 domain/src/main/java/com/mobiledevpro/domain/statistic/data/StatisticDataRepository.kt diff --git a/app/build.gradle b/app/build.gradle index ff650cb..8162e04 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'com.google.firebase.crashlytics' +apply plugin: "androidx.navigation.safeargs.kotlin" android { compileSdkVersion rootProject.compileSdkVersion @@ -108,7 +109,7 @@ android { } } - dataBinding.enabled = true + android.dataBinding.enabled = true kapt.correctErrorTypes = true } @@ -151,6 +152,7 @@ dependencies { //Rx implementation deps.rxAndroid implementation deps.rxKotlin + implementation deps.rxRelay //Glide implementation deps.glide @@ -158,13 +160,9 @@ dependencies { //Logs implementation deps.timber - // debugImplementation deps.stetho - // debugImplementation deps.flipper - // debugImplementation deps.stethoNetworkHelper - // debugImplementation deps.soloader - // releaseImplementation deps.flipperNoop - // + //Chart + implementation deps.mpaChart //Beta testing implementation deps.testfairy diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3dc0a58..b1145a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,8 @@ tools:replace="android:allowBackup"> + android:theme="@style/AppTheme.Splash" + android:screenOrientation="portrait"> diff --git a/app/src/main/java/com/mobiledevpro/app/common/App.kt b/app/src/main/java/com/mobiledevpro/app/common/App.kt index 5361272..89cbb60 100644 --- a/app/src/main/java/com/mobiledevpro/app/common/App.kt +++ b/app/src/main/java/com/mobiledevpro/app/common/App.kt @@ -5,7 +5,11 @@ import android.util.Log import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.iid.FirebaseInstanceId import com.mobiledevpro.app.BuildConfig -import com.mobiledevpro.app.di.* +import com.mobiledevpro.app.di.dataLocalModule +import com.mobiledevpro.app.di.dataModule +import com.mobiledevpro.app.di.dataRemoteModule +import com.mobiledevpro.app.di.domainModule +import com.mobiledevpro.app.di.uiModule import com.mobiledevpro.data.LOG_TAG_DEBUG import com.testfairy.TestFairy import org.koin.android.ext.koin.androidContext @@ -98,11 +102,4 @@ class App : Application() { // Timber.plant(CrashlyticsTree()) } } - - /* - companion object { - val flipperNetworkPlugin = if (BuildConfig.DEBUG) Т() else null - } - - */ } diff --git a/app/src/main/java/com/mobiledevpro/app/di/Module.kt b/app/src/main/java/com/mobiledevpro/app/di/Module.kt index 2462e3f..a699555 100644 --- a/app/src/main/java/com/mobiledevpro/app/di/Module.kt +++ b/app/src/main/java/com/mobiledevpro/app/di/Module.kt @@ -1,25 +1,38 @@ package com.mobiledevpro.app.di import com.mobiledevpro.app.ui.main.viemodel.MainViewModel +import com.mobiledevpro.app.ui.statistic.viewmodel.StatisticCountryViewModel import com.mobiledevpro.app.ui.total.viewmodel.TotalViewModel import com.mobiledevpro.app.utils.provider.DefaultResourceProvider import com.mobiledevpro.app.utils.provider.ResourceProvider -import com.mobiledevpro.data.repository.userdata.CovidCache -import com.mobiledevpro.data.repository.userdata.CovidRemote +import com.mobiledevpro.data.repository.parcer.StatisticsParserHtml +import com.mobiledevpro.data.repository.statistic.DefaultStatisticDataRepository +import com.mobiledevpro.data.repository.statistic.StatisticCovidCache +import com.mobiledevpro.data.repository.statistic.StatisticCovidRemote import com.mobiledevpro.data.repository.userdata.DefaultTotalDataRepository +import com.mobiledevpro.data.repository.userdata.TotalCovidCache +import com.mobiledevpro.data.repository.userdata.TotalCovidRemote +import com.mobiledevpro.domain.statistic.data.DefaultStatisticDataInteractor +import com.mobiledevpro.domain.statistic.data.StatisticDataInteractor +import com.mobiledevpro.domain.statistic.data.StatisticDataRepository import com.mobiledevpro.domain.totaldata.DefaultTotalDataInteractor import com.mobiledevpro.domain.totaldata.TotalDataInteractor import com.mobiledevpro.domain.totaldata.TotalDataRepository -import com.mobiledevpro.local.database.DefaultCovidCache +import com.mobiledevpro.local.database.AppDatabase +import com.mobiledevpro.local.database.statistic.DefaultStatisticsCovidCache +import com.mobiledevpro.local.database.total.DefaultTotalCovidCache import com.mobiledevpro.local.storage.PreferencesHelper import com.mobiledevpro.local.storage.PreferencesHelperImpl -import com.mobiledevpro.remote.implementation.DefaultTotalDataIRemote +import com.mobiledevpro.remote.implementation.DefaultStatisticCovidRemote +import com.mobiledevpro.remote.implementation.DefaultStatisticsParserHtml +import com.mobiledevpro.remote.implementation.DefaultTotalCovidRemote import com.mobiledevpro.remote.service.RemoteServiceFactory import com.mobiledevpro.remote.service.http.OkHttpFactory -import com.mobiledevpro.remote.service.interceptor.ApiRequestInterceptor import com.mobiledevpro.remote.service.interceptor.ApiResponseInterceptor +import com.mobiledevpro.remote.service.interceptor.ApiStatisticsRequestInterceptor import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.qualifier.named import org.koin.dsl.module /** @@ -32,36 +45,90 @@ import org.koin.dsl.module */ val uiModule = module { - viewModel { TotalViewModel(get(), get()) } + viewModel { MainViewModel() } + viewModel { + TotalViewModel( + resourceProvider = get(), + totalInteractor = get(), + statisticInteractor = get() + ) + } + + viewModel { + StatisticCountryViewModel( + resourceProvider = get(), + statisticInteractor = get() + ) + } + single { DefaultResourceProvider(androidContext().resources) as ResourceProvider } } val domainModule = module { - single { DefaultTotalDataInteractor(get()) as TotalDataInteractor } + single { DefaultTotalDataInteractor(totalDataRepository = get()) as TotalDataInteractor } + single { DefaultStatisticDataInteractor(statisticDataRepository = get()) as StatisticDataInteractor } } val dataModule = module { - single { DefaultTotalDataRepository(get(), get()) as TotalDataRepository } + single { + DefaultTotalDataRepository( + totalCovidCache = get(), + totalCovidRemote = get() + ) as TotalDataRepository + } + single { DefaultStatisticDataRepository( + statisticsCache = get(), + statisticRemote = get(), + statisticsParserHtml = get() + ) as StatisticDataRepository } } val dataLocalModule = module { - single { DefaultTotalDataIRemote(get()) as CovidRemote } - single { DefaultCovidCache(get()) as CovidCache } - single { PreferencesHelperImpl(get()) as PreferencesHelper } + single { DefaultTotalCovidRemote(apiTotal = get()) as TotalCovidRemote } + single { DefaultTotalCovidCache(database = get()) as TotalCovidCache } + + single { DefaultStatisticCovidRemote(apiStatistic = get()) as StatisticCovidRemote } + single { DefaultStatisticsCovidCache(database = get()) as StatisticCovidCache } + + single { AppDatabase.getInstance(androidContext()) } + + single { PreferencesHelperImpl(appContext = get()) as PreferencesHelper } + } val dataRemoteModule = module { - // retrofit instance, firebase database, etc - single { RemoteServiceFactory(get()).buildStackOverFlowApi() } + // retrofit instance, firebase database, html parser etc + single { + RemoteServiceFactory( + client = get(named(TOTAL_OK_HTTP_CLIENT)) + ).buildCovidTotalApi() + } single { + RemoteServiceFactory( + client = get(named(STATISTIC_OK_HTTP_CLIENT)) + ).buildCovidFullStatisticsApi() + } + + single(named(STATISTIC_OK_HTTP_CLIENT)) { OkHttpFactory().buildOkHttpClient( listOf( ApiResponseInterceptor(), - ApiRequestInterceptor() + ApiStatisticsRequestInterceptor() ) ) } + + single(named(TOTAL_OK_HTTP_CLIENT)) { + OkHttpFactory().buildOkHttpClient( + listOf(ApiResponseInterceptor()) + ) + } + + single { DefaultStatisticsParserHtml() as StatisticsParserHtml } } + +private const val TOTAL_OK_HTTP_CLIENT = "total" +private const val STATISTIC_OK_HTTP_CLIENT = "statistics" diff --git a/app/src/main/java/com/mobiledevpro/app/ui/countries/CountriesListFragment.kt b/app/src/main/java/com/mobiledevpro/app/ui/countries/CountriesListFragment.kt index 0fd8f92..e6226f6 100644 --- a/app/src/main/java/com/mobiledevpro/app/ui/countries/CountriesListFragment.kt +++ b/app/src/main/java/com/mobiledevpro/app/ui/countries/CountriesListFragment.kt @@ -13,6 +13,7 @@ import com.mobiledevpro.app.ui.countries.adapter.CountriesListAdapter import com.mobiledevpro.app.ui.main.viemodel.MainViewModel import com.mobiledevpro.app.ui.total.viewmodel.TotalViewModel import com.mobiledevpro.app.utils.Navigation +import com.mobiledevpro.app.utils.showStatisticCountry import com.mobiledevpro.commons.fragment.BaseFragment import kotlinx.android.synthetic.main.fragment_countries_list.* import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -79,7 +80,7 @@ class CountriesListFragment : BaseFragment() { rv_countries_list?.layoutManager = layoutManager rv_countries_list?.setHasFixedSize(true) - rv_countries_list?.adapter = CountriesListAdapter() + rv_countries_list?.adapter = CountriesListAdapter(this::showStatisticCountry) rv_countries_list.addItemDecoration(divider) } @@ -92,6 +93,7 @@ class CountriesListFragment : BaseFragment() { if (this.isNotEmpty()) { onActionViewExpanded() setQuery(this, true) + mainViewModel.setFabActionCloseCountrySearch() } } @@ -103,19 +105,42 @@ class CountriesListFragment : BaseFragment() { return true } }) + + setOnSearchClickListener { + mainViewModel.setFabActionCloseCountrySearch() + } } } private fun observeEvents() { mainViewModel.eventNavigateTo.observe(viewLifecycleOwner, Observer { it.peekContent().let { navigateTo -> - if (navigateTo == Navigation.NAVIGATE_TO_SEARCH_COUNTRY) { - searchView.apply { - onActionViewExpanded() - totalViewModel.getQuery().apply { - setQuery(this, true) + when (navigateTo) { + Navigation.NAVIGATE_TO_SEARCH_COUNTRY -> { + searchView.apply { + onActionViewExpanded() + totalViewModel.getQuery().apply { + setQuery(this, true) + } + } + + mainViewModel.setFabActionCloseCountrySearch() + } + + Navigation.NAVIGATE_CLOSE_SEARCH_COUNTRY -> { + searchView.apply { + onActionViewCollapsed() + totalViewModel.getQuery().apply { + setQuery(this, true) + } } + + mainViewModel.setFabActionShowCountrySearch() } + + else -> { + } + } } }) diff --git a/app/src/main/java/com/mobiledevpro/app/ui/countries/adapter/CountriesListAdapter.kt b/app/src/main/java/com/mobiledevpro/app/ui/countries/adapter/CountriesListAdapter.kt index c478ce6..05d8df9 100644 --- a/app/src/main/java/com/mobiledevpro/app/ui/countries/adapter/CountriesListAdapter.kt +++ b/app/src/main/java/com/mobiledevpro/app/ui/countries/adapter/CountriesListAdapter.kt @@ -10,7 +10,7 @@ import androidx.recyclerview.widget.RecyclerView import com.mobiledevpro.app.R import com.mobiledevpro.app.databinding.ItemCountryBinding import com.mobiledevpro.app.utils.diff.CountiesDiffUtil -import com.mobiledevpro.domain.model.Country +import com.mobiledevpro.domain.model.TotalCountry /** * Adapter for RecyclerView Countries list @@ -20,9 +20,11 @@ import com.mobiledevpro.domain.model.Country * http://androiddev.pro * */ -class CountriesListAdapter : RecyclerView.Adapter() { +class CountriesListAdapter( + private val onClick: (countryName: String) -> Unit +) : RecyclerView.Adapter() { - private var countriesList: ArrayList = ArrayList() + private var countriesList: ArrayList = ArrayList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CountryItemViewHolder = CountryItemViewHolder(parent) @@ -30,12 +32,12 @@ class CountriesListAdapter : RecyclerView.Adapter) { + fun populateList(update: ArrayList) { val callback = CountiesDiffUtil(countriesList, update) val result = DiffUtil.calculateDiff(callback) countriesList.clear() @@ -47,9 +49,9 @@ class CountriesListAdapter : RecyclerView.Adapter) { + fun RecyclerView.bindItems(items: List?) { val adapter = adapter as CountriesListAdapter - adapter.populateList(ArrayList(items)) + adapter.populateList(if (items == null) ArrayList() else ArrayList(items)) } } @@ -66,9 +68,14 @@ class CountriesListAdapter : RecyclerView.Adapter Unit + ) { binding.item = item binding.executePendingBindings() + + itemView.setOnClickListener { onClick(item.country) } } } } \ No newline at end of file diff --git a/app/src/main/java/com/mobiledevpro/app/ui/main/MainActivity.kt b/app/src/main/java/com/mobiledevpro/app/ui/main/MainActivity.kt index fb23445..451c3d4 100644 --- a/app/src/main/java/com/mobiledevpro/app/ui/main/MainActivity.kt +++ b/app/src/main/java/com/mobiledevpro/app/ui/main/MainActivity.kt @@ -11,6 +11,7 @@ import com.mobiledevpro.app.R import com.mobiledevpro.app.ui.main.viemodel.MainViewModel import com.mobiledevpro.app.utils.FabActionNavigation import com.mobiledevpro.app.utils.Navigation +import com.mobiledevpro.app.utils.show import com.mobiledevpro.app.utils.showCountiesList import com.mobiledevpro.commons.activity.BaseActivity import com.mobiledevpro.commons.helpers.BaseResourcesHelper @@ -65,7 +66,7 @@ class MainActivity : BaseActivity() { this.setOnClickListener { mainViewModel.showCountriesList() } this.setImageDrawable( BaseResourcesHelper.getDrawableCompatible(this@MainActivity, R.drawable.ic_world_24dp)) - this.show() + this.show(true) } FabActionNavigation.ACTION_SHOW_COUNTRY_SEARCH_BAR -> @@ -73,8 +74,23 @@ class MainActivity : BaseActivity() { this.setOnClickListener { mainViewModel.showSearchCountryBar() } this.setImageDrawable( BaseResourcesHelper.getDrawableCompatible(this@MainActivity, R.drawable.ic_search_24)) - this.show() + this.show(true) } + + FabActionNavigation.ACTION_CLOSE_COUNTRY_SEARCH_BAR -> + fab_main_action?.apply { + this.setOnClickListener { mainViewModel.closeSearchCountryBar() } + this.setImageDrawable( + BaseResourcesHelper.getDrawableCompatible(this@MainActivity, R.drawable.ic_close_24dp)) + this.show(true) + } + + + FabActionNavigation.ACTION_HIDE -> + fab_main_action?.apply { + this.show(false) + } + } } }) @@ -82,7 +98,7 @@ class MainActivity : BaseActivity() { } private fun applyWindowInsets(view: View) { - //Use Window Insets to set top and bottom paddings to our activity + //Use Window Insets to set top and bottom padding's to our activity ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> v.updatePadding( left = insets.systemWindowInsetLeft, diff --git a/app/src/main/java/com/mobiledevpro/app/ui/main/viemodel/MainViewModel.kt b/app/src/main/java/com/mobiledevpro/app/ui/main/viemodel/MainViewModel.kt index 68aaaeb..165533c 100644 --- a/app/src/main/java/com/mobiledevpro/app/ui/main/viemodel/MainViewModel.kt +++ b/app/src/main/java/com/mobiledevpro/app/ui/main/viemodel/MainViewModel.kt @@ -34,6 +34,11 @@ class MainViewModel : BaseViewModel() { Event(Navigation.NAVIGATE_TO_SEARCH_COUNTRY) } + fun closeSearchCountryBar() { + _eventNavigateTo.value = + Event(Navigation.NAVIGATE_CLOSE_SEARCH_COUNTRY) + } + fun setFabActionShowCountries() { _eventFabAction.value = Event(FabActionNavigation.ACTION_SHOW_COUNTRIES) @@ -44,4 +49,13 @@ class MainViewModel : BaseViewModel() { Event(FabActionNavigation.ACTION_SHOW_COUNTRY_SEARCH_BAR) } + fun setFabActionCloseCountrySearch() { + _eventFabAction.value = + Event(FabActionNavigation.ACTION_CLOSE_COUNTRY_SEARCH_BAR) + } + + fun setFabHide() { + _eventFabAction.value = + Event(FabActionNavigation.ACTION_HIDE) + } } \ No newline at end of file diff --git a/app/src/main/java/com/mobiledevpro/app/ui/statistic/StatisticCountryFragment.kt b/app/src/main/java/com/mobiledevpro/app/ui/statistic/StatisticCountryFragment.kt new file mode 100644 index 0000000..c1a096d --- /dev/null +++ b/app/src/main/java/com/mobiledevpro/app/ui/statistic/StatisticCountryFragment.kt @@ -0,0 +1,87 @@ +package com.mobiledevpro.app.ui.statistic + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.mobiledevpro.app.R +import com.mobiledevpro.app.databinding.FragmentStatisticCountryBinding +import com.mobiledevpro.app.ui.main.viemodel.MainViewModel +import com.mobiledevpro.app.ui.statistic.adapter.StatisticCountryListAdapter +import com.mobiledevpro.app.ui.statistic.viewmodel.StatisticCountryViewModel +import com.mobiledevpro.commons.fragment.BaseFragment +import kotlinx.android.synthetic.main.fragment_statistic_country.* +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +/** + * Fragment for showing statistic by country + */ +class StatisticCountryFragment : BaseFragment() { + + private val args: StatisticCountryFragmentArgs by navArgs() + + private val statisticViewModel: StatisticCountryViewModel by sharedViewModel() + private val mainViewModel: MainViewModel by sharedViewModel() + + private lateinit var bottomSheetBehaviour: BottomSheetBehavior + + override fun getLayoutResId() = R.layout.fragment_statistic_country + + override fun getAppBarTitleString() = args.countryName + + override fun getHomeAsUpIndicatorIcon(): Int = R.drawable.ic_arrow_back_white_24dp + + override fun initPresenters() { + lifecycle.addObserver(statisticViewModel) + statisticViewModel.setCountryName(args.countryName) + } + + override fun populateView(layoutView: View, savedInstanceState: Bundle?): View { + val binding = FragmentStatisticCountryBinding.bind(layoutView) + .apply { + viewModel = statisticViewModel + } + binding.lifecycleOwner = viewLifecycleOwner + + observeEvents() + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initBottomSheetView() + initRecyclerView() + } + + override fun onStart() { + super.onStart() + mainViewModel.setFabHide() + } + + private fun initRecyclerView() { + val layoutManager = LinearLayoutManager(requireContext()) + + val dividerDrawable = requireContext().getDrawable(R.drawable.list_item_divider) + val divider = DividerItemDecoration(context, layoutManager.orientation) + dividerDrawable?.let { divider.setDrawable(it) } + + rvStatistic?.layoutManager = layoutManager + rvStatistic?.setHasFixedSize(true) + rvStatistic?.adapter = StatisticCountryListAdapter() + rvStatistic.addItemDecoration(divider) + } + + private fun observeEvents() { + statisticViewModel.chartEntriesView.observe(viewLifecycleOwner, androidx.lifecycle.Observer { + chartByDays.setDada(it) + }) + + } + + private fun initBottomSheetView() { + bottomSheetBehaviour = BottomSheetBehavior.from(layout_bottom_sheet) + } +} diff --git a/app/src/main/java/com/mobiledevpro/app/ui/statistic/adapter/StatisticCountryListAdapter.kt b/app/src/main/java/com/mobiledevpro/app/ui/statistic/adapter/StatisticCountryListAdapter.kt new file mode 100644 index 0000000..2b2cf0e --- /dev/null +++ b/app/src/main/java/com/mobiledevpro/app/ui/statistic/adapter/StatisticCountryListAdapter.kt @@ -0,0 +1,63 @@ +package com.mobiledevpro.app.ui.statistic.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.BindingAdapter +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.RecyclerView +import com.mobiledevpro.app.R +import com.mobiledevpro.app.databinding.ItemStatisticCountryBinding +import com.mobiledevpro.domain.model.DayStatistic + +class StatisticCountryListAdapter : RecyclerView.Adapter() { + + private val data: ArrayList = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatisticCountryItemViewHolder = + StatisticCountryItemViewHolder(parent) + + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (holder is StatisticCountryItemViewHolder) + holder.bind(data[holder.adapterPosition]) + } + + override fun getItemCount() = data.size + + fun populateList(update: ArrayList) { + data.clear() + data.addAll(update) + notifyDataSetChanged() + } + + //it uses to bind items via xml layout (see attribute app:items in ) + companion object { + @JvmStatic + @BindingAdapter("items") + fun RecyclerView.bindItems(items: List?) { + val adapter = adapter as StatisticCountryListAdapter + + adapter.populateList((if (items != null) ArrayList(items) else ArrayList())) + } + } + + abstract inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) + + inner class StatisticCountryItemViewHolder( + private val parent: ViewGroup, + private val binding: ItemStatisticCountryBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.item_statistic_country, + parent, + false + ) + + ) : ViewHolder(binding.root) { + + fun bind(item: DayStatistic) { + binding.item = item + binding.executePendingBindings() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mobiledevpro/app/ui/statistic/viewmodel/StatisticCountryViewModel.kt b/app/src/main/java/com/mobiledevpro/app/ui/statistic/viewmodel/StatisticCountryViewModel.kt new file mode 100644 index 0000000..e6593fa --- /dev/null +++ b/app/src/main/java/com/mobiledevpro/app/ui/statistic/viewmodel/StatisticCountryViewModel.kt @@ -0,0 +1,133 @@ +package com.mobiledevpro.app.ui.statistic.viewmodel + +import androidx.lifecycle.* +import com.github.mikephil.charting.data.Entry +import com.mobiledevpro.app.common.BaseViewModel +import com.mobiledevpro.app.common.Event +import com.mobiledevpro.app.utils.provider.ResourceProvider +import com.mobiledevpro.domain.common.Result +import com.mobiledevpro.domain.model.DayStatistic +import com.mobiledevpro.domain.statistic.data.StatisticDataInteractor +import io.reactivex.rxkotlin.addTo +import io.reactivex.rxkotlin.subscribeBy + +/** + * ViewModel for statistic fragment + */ +class StatisticCountryViewModel( + private val resourceProvider: ResourceProvider, + private val statisticInteractor: StatisticDataInteractor +) : BaseViewModel(), LifecycleObserver { + + private val _eventShowError = MutableLiveData>() + val eventShowError: LiveData> = _eventShowError + + private val _isShowProgressStatistic = MutableLiveData() + val isShowProgressStatistic: LiveData = _isShowProgressStatistic + + private val _statisticCountry = MutableLiveData>() + val statisticCountry: LiveData> = _statisticCountry + + private val _chartEntries = MutableLiveData() + val chartEntriesView: LiveData = _chartEntries + + private val _totalData = MutableLiveData() + val totalData: LiveData = _totalData + + private val _lastStatisticDate = MutableLiveData() + val lastStatisticDate: LiveData = _lastStatisticDate + + + /** + * It should to be called in Fragment onCreate() or onCreateView() + */ + fun setCountryName(name: String) { + observeConfirmedList(name) + } + + private fun observeConfirmedList(query: String) { + statisticInteractor + .observeStatisticByCountryName(query = query) + .doOnSubscribe { + _isShowProgressStatistic.value = true + } + .doOnNext { + _isShowProgressStatistic.value = false + } + .subscribeBy { + when (it) { + is Result.Success -> { + _statisticCountry.value = it.data.dayStatistics.reversed() + _totalData.value = mapDayStatisticToTotal(it.data.dayStatistics.last()) + mapDaysStatisticToChartView(it.data.dayStatistics) + } + is Result.Failure -> { + val errorMessage = resourceProvider.getErrorMessage(it.error) + _eventShowError.value = Event(errorMessage) + } + } + + } + .addTo(subscriptions) + } + + private fun mapDaysStatisticToChartView(dayStatistics: List) { + val entries = ChartLinesView() + + _lastStatisticDate.value = dayStatistics.last().date + + dayStatistics.forEach { + if (it.totalConfirmed != 0L) + entries.apply { + confirmed.add( + Entry( + it.date.toFloat(), + it.totalConfirmed.toFloat() + ) + ) + death.add( + Entry( + it.date.toFloat(), + it.totalDeaths.toFloat() + ) + ) + recovered.add( + Entry( + it.date.toFloat(), + it.totalRecovered.toFloat() + ) + ) + } + + _chartEntries.value = entries + } + } + + private fun mapDayStatisticToTotal(dayStatistic: DayStatistic) = TotalStatisticView( + totalConfirmed = dayStatistic.totalConfirmed, + totalRecovered = dayStatistic.totalRecovered, + totalDeaths = dayStatistic.totalDeaths + ) + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun onStartView() { + // do something if needed + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onStopView() { + // do something if needed + } + + data class ChartLinesView( + val confirmed: ArrayList = arrayListOf(), + val death: ArrayList = arrayListOf(), + val recovered: ArrayList = arrayListOf() + ) + + data class TotalStatisticView( + val totalConfirmed: Long, + val totalDeaths: Long, + val totalRecovered: Long + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mobiledevpro/app/ui/total/TotalFragment.kt b/app/src/main/java/com/mobiledevpro/app/ui/total/TotalFragment.kt index 13c1fd8..623e9cb 100644 --- a/app/src/main/java/com/mobiledevpro/app/ui/total/TotalFragment.kt +++ b/app/src/main/java/com/mobiledevpro/app/ui/total/TotalFragment.kt @@ -8,6 +8,7 @@ import com.mobiledevpro.app.R import com.mobiledevpro.app.databinding.FragmentTotalBinding import com.mobiledevpro.app.ui.main.viemodel.MainViewModel import com.mobiledevpro.app.ui.total.viewmodel.TotalViewModel +import com.mobiledevpro.commons.activity.IBaseActivity import com.mobiledevpro.commons.fragment.BaseFragment import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -32,7 +33,7 @@ class TotalFragment : BaseFragment() { override fun getAppBarTitle() = R.string.app_name_main - override fun getHomeAsUpIndicatorIcon() = R.drawable.ic_close_24dp + override fun getHomeAsUpIndicatorIcon() = 0 override fun populateView(view: View, savedInstanceState: Bundle?): View { //databinding @@ -51,6 +52,12 @@ class TotalFragment : BaseFragment() { lifecycle.addObserver(totalViewModel) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (activity as IBaseActivity).setHomeAsUpIndicatorIcon(homeAsUpIndicatorIcon) + } + override fun onStart() { super.onStart() mainViewModel.setFabActionShowCountries() diff --git a/app/src/main/java/com/mobiledevpro/app/ui/total/viewmodel/TotalViewModel.kt b/app/src/main/java/com/mobiledevpro/app/ui/total/viewmodel/TotalViewModel.kt index 93612d6..9ec30eb 100644 --- a/app/src/main/java/com/mobiledevpro/app/ui/total/viewmodel/TotalViewModel.kt +++ b/app/src/main/java/com/mobiledevpro/app/ui/total/viewmodel/TotalViewModel.kt @@ -7,9 +7,9 @@ import com.mobiledevpro.app.utils.dateToSting import com.mobiledevpro.app.utils.provider.ResourceProvider import com.mobiledevpro.app.utils.toDecimalFormat import com.mobiledevpro.domain.common.Result -import com.mobiledevpro.domain.model.Country +import com.mobiledevpro.domain.model.TotalCountry +import com.mobiledevpro.domain.statistic.data.StatisticDataInteractor import com.mobiledevpro.domain.totaldata.TotalDataInteractor -import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo import io.reactivex.rxkotlin.subscribeBy @@ -23,12 +23,11 @@ import io.reactivex.rxkotlin.subscribeBy * #MobileDevPro */ class TotalViewModel( - private val interactor: TotalDataInteractor, - private val resourceProvider: ResourceProvider + private val resourceProvider: ResourceProvider, + private val totalInteractor: TotalDataInteractor, + private val statisticInteractor: StatisticDataInteractor ) : BaseViewModel(), LifecycleObserver { - private val localSubscriptions = CompositeDisposable() - private var query: String = "" private val _isShowProgressTotalConfirmed = MutableLiveData() @@ -52,26 +51,22 @@ class TotalViewModel( private val _updateTime = MutableLiveData() val updateTime: LiveData = _updateTime - private val _countriesList = MutableLiveData>() - val countriesList: LiveData> = _countriesList - - /* private val _eventNavigateTo = MutableLiveData>() - val eventNavigateTo: LiveData> = _eventNavigateTo + private val _countriesList = MutableLiveData>() + val countriesList: LiveData> = _countriesList - - */ private val _eventShowError = MutableLiveData>() val eventShowError: LiveData> = _eventShowError init { observeTotalValues() observeCountriesList() + fetchStatisticFromHtml() } @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onStartView() { - interactor.apply { + totalInteractor.apply { refreshTotalData() .subscribeBy { result -> when (result) { @@ -90,11 +85,6 @@ class TotalViewModel( } } - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - fun onStopView() { - //do something if needed - } - fun getQuery() = query fun getCountiesByQuery(query: String) { @@ -103,7 +93,7 @@ class TotalViewModel( } private fun observeTotalValues() { - interactor.observeTotalData() + totalInteractor.observeTotalData() .doOnSubscribe { _isShowProgressTotalConfirmed.value = true _isShowProgressTotalDeaths.value = true @@ -130,9 +120,8 @@ class TotalViewModel( } private fun observeCountriesList() { - localSubscriptions.clear() - interactor.observeCountriesListData(query) + totalInteractor.observeCountriesListData(query) .subscribeBy { result -> when (result) { is Result.Success -> _countriesList.value = result.data @@ -141,11 +130,13 @@ class TotalViewModel( } } } - .addTo(localSubscriptions) + .addTo(subscriptions) } - override fun onCleared() { - super.onCleared() - localSubscriptions.clear() + private fun fetchStatisticFromHtml() { + statisticInteractor + .fetchStatisticsFromHtml() + .subscribeBy { /* do nothing */ } + .addTo(subscriptions) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/mobiledevpro/app/ui/widget/StatisticLineChart.kt b/app/src/main/java/com/mobiledevpro/app/ui/widget/StatisticLineChart.kt new file mode 100644 index 0000000..84c75ed --- /dev/null +++ b/app/src/main/java/com/mobiledevpro/app/ui/widget/StatisticLineChart.kt @@ -0,0 +1,148 @@ +package com.mobiledevpro.app.ui.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.core.content.ContextCompat +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.data.LineData +import com.github.mikephil.charting.data.LineDataSet +import com.github.mikephil.charting.formatter.ValueFormatter +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet +import com.mobiledevpro.app.R +import com.mobiledevpro.app.ui.statistic.viewmodel.StatisticCountryViewModel +import com.mobiledevpro.app.utils.toNumberWithAbbreviation +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.TimeUnit + +class StatisticLineChart @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val lineChart = LineChart(context) + + init { + + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + lineChart.layoutParams = params + this.addView(lineChart) + + setupChart() + } + + private fun setupChart() { + lineChart.apply { + description.isEnabled = false + setTouchEnabled(true) + isDragEnabled = true + setScaleEnabled(true) + legend.isEnabled = false + + lineChart.xAxis.apply { + position = XAxis.XAxisPosition.BOTTOM + textColor = ContextCompat.getColor(context, android.R.color.white) + valueFormatter = object : ValueFormatter() { + override fun getFormattedValue(value: Float): String { + val millis = TimeUnit.MILLISECONDS.toMillis(value.toLong()) + return DATE_FORMAT.format(Date(millis)) + } + } + } + + lineChart.axisLeft.apply { + axisMinimum = 0f + setDrawAxisLine(false) + textColor = ContextCompat.getColor(context, android.R.color.white) + valueFormatter = object : ValueFormatter() { + override fun getFormattedValue(value: Float): String = + value.toNumberWithAbbreviation() + } + } + + lineChart.axisRight.apply { + isEnabled = false + } + } + + lineChart.invalidate() + } + + fun setDada(chartLinesView: StatisticCountryViewModel.ChartLinesView) { + lineChart.highlightValue(null) + lineChart.clear() + + val confirmedLine: ILineDataSet = createConfirmedChartLine(chartLinesView.confirmed) + val deathsLine: ILineDataSet = createDeathsChartLine(chartLinesView.death) + val recoveredLine: ILineDataSet = createRecoveredChartLine(chartLinesView.recovered) + + val sets = arrayListOf( + confirmedLine, + deathsLine, + recoveredLine + ) + + if (sets.isNotEmpty()) { + val linesData = LineData(sets) + lineChart.data = linesData + } + + lineChart.invalidate() + } + + private fun createConfirmedChartLine(data: ArrayList): LineDataSet { + val lineDataSet = LineDataSet(data, "") + lineDataSet.apply { + lineWidth = 2f + setDrawCircles(false) + setDrawCircleHole(false) + setDrawValues(false) + fillAlpha = 65 + color = ContextCompat.getColor(context, R.color.colorTextPrimaryRed) + highLightColor = ContextCompat.getColor(context, android.R.color.white) + } + return lineDataSet + } + + private fun createDeathsChartLine(data: ArrayList): LineDataSet { + val lineDataSet = LineDataSet(data, "") + lineDataSet.apply { + lineWidth = 2f + setDrawCircles(false) + setDrawCircleHole(false) + setDrawValues(false) + fillAlpha = 65 + color = ContextCompat.getColor(context, R.color.colorTextSecondary) + highLightColor = ContextCompat.getColor(context, android.R.color.white) + } + return lineDataSet + } + + private fun createRecoveredChartLine(data: ArrayList): LineDataSet { + val lineDataSet = LineDataSet(data, "") + lineDataSet.apply { + lineWidth = 2f + setDrawCircles(false) + setDrawCircleHole(false) + setDrawValues(false) + fillAlpha = 65 + color = ContextCompat.getColor(context, R.color.colorTextPrimaryGreen) + highLightColor = ContextCompat.getColor(context, android.R.color.white) + } + return lineDataSet + } + + + private companion object { + val DATE_FORMAT = SimpleDateFormat("MMM dd", Locale.getDefault()) + } +} diff --git a/app/src/main/java/com/mobiledevpro/app/utils/DateExtensions.kt b/app/src/main/java/com/mobiledevpro/app/utils/DateExtensions.kt index 28eb579..914f252 100644 --- a/app/src/main/java/com/mobiledevpro/app/utils/DateExtensions.kt +++ b/app/src/main/java/com/mobiledevpro/app/utils/DateExtensions.kt @@ -4,6 +4,12 @@ import java.text.SimpleDateFormat import java.util.* fun Long.dateToSting(): String { - val dateFormat = SimpleDateFormat(" E, dd MMM yyyy hh:mm a", Locale.getDefault()) + val dateFormat = SimpleDateFormat("MMM dd, yyyy | hh:mm", Locale.getDefault()) + return dateFormat.format(this) +} + +fun Long.toDayMonth(): String { + if (this <= 0) return "" + val dateFormat = SimpleDateFormat("MMM dd", Locale.getDefault()) return dateFormat.format(this) } \ No newline at end of file diff --git a/app/src/main/java/com/mobiledevpro/app/utils/FormatExtensions.kt b/app/src/main/java/com/mobiledevpro/app/utils/FormatExtensions.kt index 30023a1..3547efb 100644 --- a/app/src/main/java/com/mobiledevpro/app/utils/FormatExtensions.kt +++ b/app/src/main/java/com/mobiledevpro/app/utils/FormatExtensions.kt @@ -6,4 +6,18 @@ fun Int.toDecimalFormat(): String { if (this <= 0) return "0" val formatter = DecimalFormat("#,###,###") return formatter.format(this) -} \ No newline at end of file +} + +fun Long.toDecimalFormat(): String { + if (this <= 0) return "0" + val formatter = DecimalFormat("#,###,###") + return formatter.format(this) +} + +fun Float.toNumberWithAbbreviation(): String = + when { + this > 999 -> "${this.toInt() / 1000}K" + this > 999999 -> "${this.toInt() / 1000000}M" + this > 999999999 -> "${this.toInt() / 1000000000}B" + else -> "${this.toInt()}" + } diff --git a/app/src/main/java/com/mobiledevpro/app/utils/NavigationExtensions.kt b/app/src/main/java/com/mobiledevpro/app/utils/NavigationExtensions.kt index 1fa06aa..04264db 100644 --- a/app/src/main/java/com/mobiledevpro/app/utils/NavigationExtensions.kt +++ b/app/src/main/java/com/mobiledevpro/app/utils/NavigationExtensions.kt @@ -1,10 +1,13 @@ package com.mobiledevpro.app.utils import android.app.Activity +import android.view.animation.LinearInterpolator import androidx.fragment.app.Fragment import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController +import com.google.android.material.floatingactionbutton.FloatingActionButton import com.mobiledevpro.app.R +import com.mobiledevpro.app.ui.countries.CountriesListFragmentDirections /** * Navigation Helper @@ -18,16 +21,35 @@ import com.mobiledevpro.app.R enum class Navigation { NAVIGATE_TO_COUNTRIES_LIST, - NAVIGATE_TO_SEARCH_COUNTRY + NAVIGATE_TO_SEARCH_COUNTRY, + NAVIGATE_CLOSE_SEARCH_COUNTRY } enum class FabActionNavigation { + ACTION_HIDE, ACTION_SHOW_COUNTRIES, - ACTION_SHOW_COUNTRY_SEARCH_BAR + ACTION_SHOW_COUNTRY_SEARCH_BAR, + ACTION_CLOSE_COUNTRY_SEARCH_BAR, } fun Fragment.showCountiesList() = this.findNavController().navigate(R.id.actionShowCountriesList) +fun Fragment.showStatisticCountry(query: String) { + val action = CountriesListFragmentDirections.actionCountriesListFragmentToStatisticFragment(query) + this.findNavController().navigate(action) +} + fun Activity.showCountiesList(fragmentContainerId: Int) = - this.findNavController(fragmentContainerId).navigate(R.id.actionShowCountriesList) \ No newline at end of file + this.findNavController(fragmentContainerId).navigate(R.id.actionShowCountriesList) + + +fun FloatingActionButton.show(visible: Boolean) { + this.animate().apply { + scaleX(if (visible) 1F else 0F) + scaleY(if (visible) 1F else 0F) + duration = 200 + interpolator = LinearInterpolator() + start() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mobiledevpro/app/utils/diff/CountiesDiffUtil.kt b/app/src/main/java/com/mobiledevpro/app/utils/diff/CountiesDiffUtil.kt index 709a873..580503a 100644 --- a/app/src/main/java/com/mobiledevpro/app/utils/diff/CountiesDiffUtil.kt +++ b/app/src/main/java/com/mobiledevpro/app/utils/diff/CountiesDiffUtil.kt @@ -1,11 +1,11 @@ package com.mobiledevpro.app.utils.diff import androidx.recyclerview.widget.DiffUtil -import com.mobiledevpro.domain.model.Country +import com.mobiledevpro.domain.model.TotalCountry class CountiesDiffUtil( - private val old: List, - private val new: List + private val old: List, + private val new: List ) : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = diff --git a/app/src/main/java/com/mobiledevpro/app/utils/diff/StatisticCountryDiffUtil.kt b/app/src/main/java/com/mobiledevpro/app/utils/diff/StatisticCountryDiffUtil.kt new file mode 100644 index 0000000..520f909 --- /dev/null +++ b/app/src/main/java/com/mobiledevpro/app/utils/diff/StatisticCountryDiffUtil.kt @@ -0,0 +1,20 @@ +package com.mobiledevpro.app.utils.diff + +import androidx.recyclerview.widget.DiffUtil +import com.mobiledevpro.domain.model.DayStatistic + +class StatisticCountryDiffUtil( + private val old: List, + private val new: List +) : DiffUtil.Callback() { + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + old[oldItemPosition].date == new[newItemPosition].date + + override fun getOldListSize(): Int = old.size + + override fun getNewListSize(): Int = new.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + old[oldItemPosition] == new[newItemPosition] +} \ No newline at end of file diff --git a/app/src/main/java/com/mobiledevpro/app/utils/provider/DefaultResourceProvider.kt b/app/src/main/java/com/mobiledevpro/app/utils/provider/DefaultResourceProvider.kt index a02eb6a..f98563c 100644 --- a/app/src/main/java/com/mobiledevpro/app/utils/provider/DefaultResourceProvider.kt +++ b/app/src/main/java/com/mobiledevpro/app/utils/provider/DefaultResourceProvider.kt @@ -4,18 +4,23 @@ import android.content.res.Resources import com.mobiledevpro.app.R import com.mobiledevpro.domain.common.Error import com.mobiledevpro.domain.common.Error.ACCESS_DENIED_ERROR +import com.mobiledevpro.domain.common.Error.HTML_PARSER_ERROR import com.mobiledevpro.domain.common.Error.NETWORK_ERROR import com.mobiledevpro.domain.common.Error.NOT_FOUND_ERROR import com.mobiledevpro.domain.common.Error.SERVICE_UNAVAILABLE_ERROR import com.mobiledevpro.domain.common.Error.UNKNOWN_ERROR -class DefaultResourceProvider(private val resources: Resources) : ResourceProvider { +class DefaultResourceProvider( + private val resources: Resources +) : ResourceProvider { + override fun getErrorMessage(error: Error): String = resources.getString( when (error.name) { NETWORK_ERROR.name -> R.string.network_error NOT_FOUND_ERROR.name -> R.string.not_found_error ACCESS_DENIED_ERROR.name -> R.string.access_denied_error SERVICE_UNAVAILABLE_ERROR.name -> R.string.service_unavailable_error + HTML_PARSER_ERROR.name -> R.string.parsing_error UNKNOWN_ERROR.name -> R.string.unknown_error else -> throw IllegalArgumentException("Illegal parameter!") } diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml new file mode 100644 index 0000000..b449a4c --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layouts/activity/layout/activity_main.xml b/app/src/main/res/layouts/activity/layout/activity_main.xml index 0f2e8ab..6809864 100644 --- a/app/src/main/res/layouts/activity/layout/activity_main.xml +++ b/app/src/main/res/layouts/activity/layout/activity_main.xml @@ -25,7 +25,8 @@ android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="@dimen/activity_horizontal_margin" - android:visibility="gone" + android:scaleX="0" + android:scaleY="0" app:srcCompat="@drawable/ic_world_24dp" /> diff --git a/app/src/main/res/layouts/activity/layout/activity_splash.xml b/app/src/main/res/layouts/activity/layout/activity_splash.xml index 9545d7c..b13de31 100644 --- a/app/src/main/res/layouts/activity/layout/activity_splash.xml +++ b/app/src/main/res/layouts/activity/layout/activity_splash.xml @@ -6,13 +6,15 @@ android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="com.mobiledevpro.app.ui.common.SplashActivity" - tools:showIn="@layout/activity_splash"> + tools:showIn="@layout/activity_splash" + android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingStart="@dimen/activity_horizontal_margin" + android:paddingEnd="@dimen/activity_horizontal_margin"> + type="com.mobiledevpro.domain.model.TotalCountry" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layouts/fragment/layout/fragment_countries_list.xml b/app/src/main/res/layouts/fragment/layout/fragment_countries_list.xml index 89e3be4..951bea8 100644 --- a/app/src/main/res/layouts/fragment/layout/fragment_countries_list.xml +++ b/app/src/main/res/layouts/fragment/layout/fragment_countries_list.xml @@ -20,6 +20,7 @@ android:id="@+id/rv_countries_list" android:layout_width="match_parent" android:layout_height="match_parent" + tools:listitem="@layout/item_country" app:items="@{viewModel.countriesList}" /> diff --git a/app/src/main/res/layouts/fragment/layout/fragment_statistic_country.xml b/app/src/main/res/layouts/fragment/layout/fragment_statistic_country.xml new file mode 100644 index 0000000..b8e1250 --- /dev/null +++ b/app/src/main/res/layouts/fragment/layout/fragment_statistic_country.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layouts/fragment/layout/fragment_total.xml b/app/src/main/res/layouts/fragment/layout/fragment_total.xml index d8959d9..ccd90dd 100644 --- a/app/src/main/res/layouts/fragment/layout/fragment_total.xml +++ b/app/src/main/res/layouts/fragment/layout/fragment_total.xml @@ -16,14 +16,17 @@ android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.mobiledevpro.app.ui.common.SplashActivity" - tools:showIn="@layout/activity_splash"> + tools:showIn="@layout/activity_main" + android:paddingStart="@dimen/activity_horizontal_margin" + android:paddingEnd="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin"> + android:label="fragment_total_values" + tools:layout="@layout/fragment_total"> + android:label="fragment_countries" + tools:layout="@layout/fragment_countries_list"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4ee9c07..41435fe 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -5,10 +5,11 @@ Confirmed per Country v%s - Global Data \non the COVID-19 (Corona Virus) outbreak - Please, don\'t panic. Everything will be fine. - This tracker displays the near real-time status based on the Johns Hopkins University(JHU) datasource. + Global Data on the COVID-19 (Corona Virus) outbreak + Stay at home! And be healthy. + This tracker displays the near real-time status of Corova Virus outbreak based on the Johns Hopkins University(JHU) datasource. + Total on %s Total Confirmed Total Deaths Total Recovered @@ -25,4 +26,5 @@ Access Denied Service Unavailable Unknown Error + Error getting data for statistic diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f61eb19..5bc1d9f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -5,10 +5,11 @@ Заражения по странам v%s - Глобальные данные \nпо распространению COVID-19 (Corona Virus) - Пожалуйста, не паникуйте. Все будет хорошо. - Этот трекер отображает статистику распростарнения Корона Вирус почти в реальном времени, из источников Университета Джона Хопкинса (JNU). + Глобальные данные по распространению COVID-19 (Corona Virus) + Оставайтесь дома! И не болейте. + Этот трекер отображает статистику распростарнения вируса COVID-19 почти в реальном времени, из источников Университета Джона Хопкинса (JNU). + Всего на %s Всего Зараженных Всего Смертей Всего Вылечено @@ -25,4 +26,5 @@ Доступ запрещен Сервис недоступен Неизвестная ошибка + Ошибка при загрузке данных для статистики diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 80ae6a6..7ca3baa 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -6,9 +6,10 @@ v%s Глобальні дані \nпо поширенню COVID-19 (Corona Virus) - Будь ласка, не панікуйте. Усе буде добре. - Цей трекер відображає статус поширення Корона Вірус майже в реальному часі, з джерел Університету Джона Хопкінса (JNU). + Залишайтесь у дома! Та не хворійте. + Цей трекер відображає статус поширення вірусу COVID-19 майже в реальному часі, з джерел Університету Джона Хопкінса (JNU). + Усього на %s Усього Заражених Усього Смертей Усього Вилікувано @@ -25,4 +26,5 @@ Доступ заборонено Сервіс недоступний Невідома помилка + Помилка при завантаженні данних для статистики diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index b190b83..08d2273 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -4,7 +4,7 @@ #222327 #222327 - #222327 + #2B2C2F #7bb974 #d6d6d6 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8396276..41435fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,10 +5,11 @@ Confirmed per Country v%s - Global Data \non the COVID-19 (Corona Virus) outbreak - Please, don\'t panic. Everything will be fine. + Global Data on the COVID-19 (Corona Virus) outbreak + Stay at home! And be healthy. This tracker displays the near real-time status of Corova Virus outbreak based on the Johns Hopkins University(JHU) datasource. + Total on %s Total Confirmed Total Deaths Total Recovered @@ -25,4 +26,5 @@ Access Denied Service Unavailable Unknown Error + Error getting data for statistic diff --git a/build.gradle b/build.gradle index 6dd9678..be8f029 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,11 @@ buildscript { - ext.kotlinVersion = '1.3.70' + ext.kotlinVersion = '1.3.71' ext.gradleVersion = '4.0.0-beta03' ext.playServicesVersion = '4.3.3' ext.crashlyticsPluginVersion = '2.0.0-beta02' + ext.navVersion = "2.3.0-alpha04" repositories { google() @@ -17,6 +18,8 @@ buildscript { classpath "com.google.gms:google-services:$playServicesVersion" classpath "com.google.firebase:firebase-crashlytics-gradle:$crashlyticsPluginVersion" + + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion" } } @@ -26,6 +29,10 @@ allprojects { jcenter() maven { url 'https://jitpack.io' } mavenCentral() + maven { + url 'http://www.idescout.com/maven/repo/' + name 'IDEScout, Inc.' + } } } @@ -65,6 +72,9 @@ ext { flipperVersion = '0.33.1' soloaderVersion = "0.8.2" testfairyVersion = '1.+@aar' + jsoupVersion = "1.13.1" + mpaChartVersion = "v3.1.0" + rxRelayVersion = "2.1.1" firebaseMessagingVersion = "20.1.4" deps = [ @@ -84,6 +94,8 @@ ext { "fcm" : "com.google.firebase:firebase-messaging:$firebaseMessagingVersion", "coreKtx" : "androidx.core:core-ktx:$coreKtxVersion", "kotlin" : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion", + "rxRelay" : "com.jakewharton.rxrelay2:rxrelay:$rxRelayVersion", + // ViewModel and LiveData "lifecycle" : "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion", @@ -122,7 +134,13 @@ ext { "stethoNetworkHelper" : "com.facebook.stetho:stetho-okhttp3:$stethoVersion", "soloader" : "com.facebook.soloader:soloader:$soloaderVersion", + //HTML parser + "jsoup" : "org.jsoup:jsoup:$jsoupVersion", + //Beta testing - "testfairy" : "testfairy:testfairy-android-sdk:$testfairyVersion" + "testfairy" : "testfairy:testfairy-android-sdk:$testfairyVersion", + + //MPAAndroidChart + "mpaChart" : "com.github.PhilJay:MPAndroidChart:$mpaChartVersion" ] } diff --git a/data-local/build.gradle b/data-local/build.gradle index f9a0f14..a6f8a65 100644 --- a/data-local/build.gradle +++ b/data-local/build.gradle @@ -11,7 +11,7 @@ android.defaultConfig.javaCompileOptions { android.productFlavors { production { - buildConfigField "int", "RoomDatabaseVersion", "2" + buildConfigField "int", "RoomDatabaseVersion", "3" } } diff --git a/data-local/schemas/com.mobiledevpro.local.database.AppDatabase/2.json b/data-local/schemas/com.mobiledevpro.local.database.AppDatabase/2.json index 3d54da5..76ae46b 100644 --- a/data-local/schemas/com.mobiledevpro.local.database.AppDatabase/2.json +++ b/data-local/schemas/com.mobiledevpro.local.database.AppDatabase/2.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "a4e968e459a698eaaaa43c073e1d9702", + "identityHash": "3e9d516bddbbd8324dcf4986fa5cd036", "entities": [ { "tableName": "total", @@ -49,7 +49,7 @@ "foreignKeys": [] }, { - "tableName": "counties", + "tableName": "counties_total", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `country` TEXT NOT NULL, `updated` INTEGER NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `confirmed` INTEGER NOT NULL, `deaths` INTEGER NOT NULL, `recovered` INTEGER NOT NULL, `active` INTEGER NOT NULL, PRIMARY KEY(`id`))", "fields": [ { @@ -115,20 +115,121 @@ }, "indices": [ { - "name": "index_counties_id", + "name": "index_counties_total_id", "unique": false, "columnNames": [ "id" ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_counties_id` ON `${TABLE_NAME}` (`id`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_counties_total_id` ON `${TABLE_NAME}` (`id`)" }, { - "name": "index_counties_country", + "name": "index_counties_total_country", "unique": false, "columnNames": [ "country" ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_counties_country` ON `${TABLE_NAME}` (`country`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_counties_total_country` ON `${TABLE_NAME}` (`country`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "countries_statistic", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`country` TEXT NOT NULL, `province` TEXT NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, PRIMARY KEY(`province`))", + "fields": [ + { + "fieldPath": "country", + "columnName": "country", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "province", + "columnName": "province", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "province" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_countries_statistic_province", + "unique": false, + "columnNames": [ + "province" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_countries_statistic_province` ON `${TABLE_NAME}` (`province`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "day_total_country_statistic", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`province` TEXT NOT NULL, `date` INTEGER NOT NULL, `confirmed` INTEGER NOT NULL, `deaths` INTEGER NOT NULL, `recovered` INTEGER NOT NULL, PRIMARY KEY(`date`, `province`))", + "fields": [ + { + "fieldPath": "province", + "columnName": "province", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "confirmed", + "columnName": "confirmed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deaths", + "columnName": "deaths", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recovered", + "columnName": "recovered", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "date", + "province" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_day_total_country_statistic_date", + "unique": false, + "columnNames": [ + "date" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_day_total_country_statistic_date` ON `${TABLE_NAME}` (`date`)" } ], "foreignKeys": [] @@ -137,7 +238,7 @@ "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, 'a4e968e459a698eaaaa43c073e1d9702')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e9d516bddbbd8324dcf4986fa5cd036')" ] } } \ No newline at end of file diff --git a/data-local/schemas/com.mobiledevpro.local.database.AppDatabase/3.json b/data-local/schemas/com.mobiledevpro.local.database.AppDatabase/3.json new file mode 100644 index 0000000..f46f840 --- /dev/null +++ b/data-local/schemas/com.mobiledevpro.local.database.AppDatabase/3.json @@ -0,0 +1,244 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "3e9d516bddbbd8324dcf4986fa5cd036", + "entities": [ + { + "tableName": "total", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `confirmed` INTEGER NOT NULL, `deaths` INTEGER NOT NULL, `recovered` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "confirmed", + "columnName": "confirmed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deaths", + "columnName": "deaths", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recovered", + "columnName": "recovered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "counties_total", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `country` TEXT NOT NULL, `updated` INTEGER NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `confirmed` INTEGER NOT NULL, `deaths` INTEGER NOT NULL, `recovered` INTEGER NOT NULL, `active` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "country", + "columnName": "country", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updated", + "columnName": "updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "confirmed", + "columnName": "confirmed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deaths", + "columnName": "deaths", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recovered", + "columnName": "recovered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_counties_total_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_counties_total_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_counties_total_country", + "unique": false, + "columnNames": [ + "country" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_counties_total_country` ON `${TABLE_NAME}` (`country`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "countries_statistic", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`country` TEXT NOT NULL, `province` TEXT NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, PRIMARY KEY(`province`))", + "fields": [ + { + "fieldPath": "country", + "columnName": "country", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "province", + "columnName": "province", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "province" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_countries_statistic_province", + "unique": false, + "columnNames": [ + "province" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_countries_statistic_province` ON `${TABLE_NAME}` (`province`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "day_total_country_statistic", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`province` TEXT NOT NULL, `date` INTEGER NOT NULL, `confirmed` INTEGER NOT NULL, `deaths` INTEGER NOT NULL, `recovered` INTEGER NOT NULL, PRIMARY KEY(`date`, `province`))", + "fields": [ + { + "fieldPath": "province", + "columnName": "province", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "confirmed", + "columnName": "confirmed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deaths", + "columnName": "deaths", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recovered", + "columnName": "recovered", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "date", + "province" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_day_total_country_statistic_date", + "unique": false, + "columnNames": [ + "date" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_day_total_country_statistic_date` ON `${TABLE_NAME}` (`date`)" + } + ], + "foreignKeys": [] + } + ], + "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, '3e9d516bddbbd8324dcf4986fa5cd036')" + ] + } +} \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/AppDatabase.kt b/data-local/src/main/java/com/mobiledevpro/local/database/AppDatabase.kt index 38c17c7..7a81000 100644 --- a/data-local/src/main/java/com/mobiledevpro/local/database/AppDatabase.kt +++ b/data-local/src/main/java/com/mobiledevpro/local/database/AppDatabase.kt @@ -5,10 +5,14 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.mobiledevpro.local.BuildConfig -import com.mobiledevpro.local.database.dao.CountryDataDao -import com.mobiledevpro.local.database.dao.TotalDataDao -import com.mobiledevpro.local.database.model.CachedCounties -import com.mobiledevpro.local.database.model.CachedTotal +import com.mobiledevpro.local.database.statistic.dao.StatisticCountryDataDao +import com.mobiledevpro.local.database.statistic.dao.StatisticDayCountryDataDao +import com.mobiledevpro.local.database.statistic.model.CachedDayTotalCountryStatistic +import com.mobiledevpro.local.database.statistic.model.CachedStatisticCountry +import com.mobiledevpro.local.database.total.dao.TotalCountryDataDao +import com.mobiledevpro.local.database.total.dao.TotalDataDao +import com.mobiledevpro.local.database.total.model.CachedTotal +import com.mobiledevpro.local.database.total.model.CachedTotalCounties /** * Room Database @@ -23,15 +27,20 @@ import com.mobiledevpro.local.database.model.CachedTotal @Database( entities = [ CachedTotal::class, - CachedCounties::class + CachedTotalCounties::class, + CachedStatisticCountry::class, + CachedDayTotalCountryStatistic::class ], version = BuildConfig.RoomDatabaseVersion, exportSchema = true ) -internal abstract class AppDatabase : RoomDatabase() { - internal abstract val totalDataDao: TotalDataDao - internal abstract val countiesDataDao: CountryDataDao +abstract class AppDatabase : RoomDatabase() { + abstract val totalDataDao: TotalDataDao + abstract val totalCountryDataDao: TotalCountryDataDao + abstract val statisticCountryData: StatisticCountryDataDao + abstract val statisticDayCountryData: StatisticDayCountryDataDao + companion object { @Volatile @@ -43,10 +52,10 @@ internal abstract class AppDatabase : RoomDatabase() { if (instance == null) { instance = Room.databaseBuilder( - context.applicationContext, - AppDatabase::class.java, - "app_database" - ) + context.applicationContext, + AppDatabase::class.java, + "app_database" + ) .fallbackToDestructiveMigration() .build() INSTANCE = instance diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/DefaultCovidCache.kt b/data-local/src/main/java/com/mobiledevpro/local/database/DefaultCovidCache.kt deleted file mode 100644 index 4ef8d8d..0000000 --- a/data-local/src/main/java/com/mobiledevpro/local/database/DefaultCovidCache.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.mobiledevpro.local.database - -import android.content.Context -import com.mobiledevpro.data.model.CountryEntity -import com.mobiledevpro.data.model.TotalEntity -import com.mobiledevpro.data.repository.userdata.CovidCache -import com.mobiledevpro.local.database.model.CachedCounties -import com.mobiledevpro.local.database.model.CachedTotal -import com.mobiledevpro.local.mapper.toCached -import com.mobiledevpro.local.mapper.toEntity -import io.reactivex.Completable -import io.reactivex.Observable - -/** - * Database Helper - * - * Created by Dmitriy Chernysh - * - * http://androiddev.pro - * - * #MobileDevPro - */ -class DefaultCovidCache(private val appContext: Context) : CovidCache { - - override fun getTotalDataObservable(): Observable = - AppDatabase.getInstance(appContext) - .totalDataDao - .getTotalDataObservable() - .map(CachedTotal::toEntity) - - override fun updateTotalData(totalEntity: TotalEntity) = Completable - .create { emitter -> - val dao = AppDatabase.getInstance(appContext) - .totalDataDao - - dao.deleteAllTotalValues() - - dao.insert(totalEntity.toCached()) - - emitter.onComplete() - } - - override fun getLocalCountriesObservable(query: String): Observable> = - AppDatabase.getInstance(appContext) - .countiesDataDao - .getCountiesDataObservable(query) - .map { it.map(CachedCounties::toEntity) } - - override fun updateCountries(countriesEntity: List) = Completable - .create { emitter -> - val dao = AppDatabase.getInstance(appContext) - .totalDataDao - - dao.deleteAllTotalValues() - - val countriesCached = countriesEntity.map(CountryEntity::toCached) - AppDatabase.getInstance(appContext) - .countiesDataDao - .insert(countriesCached) - - emitter.onComplete() - } -} diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/dao/CountryDataDao.kt b/data-local/src/main/java/com/mobiledevpro/local/database/dao/CountryDataDao.kt deleted file mode 100644 index 714d2fd..0000000 --- a/data-local/src/main/java/com/mobiledevpro/local/database/dao/CountryDataDao.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.mobiledevpro.local.database.dao - -import androidx.room.Dao -import androidx.room.Query -import com.mobiledevpro.local.database.model.CachedCounties -import io.reactivex.Observable - -@Dao -internal interface CountryDataDao : BaseDao { - - @Query("SELECT * FROM counties WHERE country LIKE '%' || :query || '%' ORDER BY confirmed DESC") - fun getCountiesDataObservable(query: String): Observable> - - @Query("DELETE FROM counties") - fun deleteAllCountriesValues() -} \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/statistic/DefaultStatisticsCovidCache.kt b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/DefaultStatisticsCovidCache.kt new file mode 100644 index 0000000..1265812 --- /dev/null +++ b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/DefaultStatisticsCovidCache.kt @@ -0,0 +1,44 @@ +package com.mobiledevpro.local.database.statistic + +import com.mobiledevpro.data.model.statistic.StatisticEntity +import com.mobiledevpro.data.repository.statistic.StatisticCovidCache +import com.mobiledevpro.local.database.AppDatabase +import com.mobiledevpro.local.database.statistic.model.CachedDayTotalCountryStatistic +import com.mobiledevpro.local.database.statistic.model.CachedStatisticCountryWithDailyStatistic +import com.mobiledevpro.local.mapper.toCache +import com.mobiledevpro.local.mapper.toEntity +import io.reactivex.Completable +import io.reactivex.Observable + +class DefaultStatisticsCovidCache( + private val database: AppDatabase +) : StatisticCovidCache { + + override fun updateConfirmedData(statistics: ArrayList) = Completable + .defer { + + val countiesCache = statistics.map { it.toCache() } + + val dayStatisticsCache = statistics.map { statisticEntity -> + statisticEntity.dayStatistic.map { dayTotalEntity -> + CachedDayTotalCountryStatistic( + province = statisticEntity.country.provinceName, + date = dayTotalEntity.date, + confirmed = dayTotalEntity.confirmed, + deaths = dayTotalEntity.deaths, + recovered = dayTotalEntity.recovered + ) + } + } + + database.statisticCountryData.insert(countiesCache) + + database.statisticDayCountryData.insert(dayStatisticsCache.flatten()) + + Completable.complete() + } + + override fun observeConfirmedDataByCountryName(query: String): Observable> = database + .statisticCountryData.getCountryByCountryName(query) + .map { it.map(CachedStatisticCountryWithDailyStatistic::toEntity) } +} diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/statistic/dao/StatisticCountryDataDao.kt b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/dao/StatisticCountryDataDao.kt new file mode 100644 index 0000000..57ce5ed --- /dev/null +++ b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/dao/StatisticCountryDataDao.kt @@ -0,0 +1,20 @@ +package com.mobiledevpro.local.database.statistic.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import com.mobiledevpro.local.database.statistic.model.CachedStatisticCountry +import com.mobiledevpro.local.database.statistic.model.CachedStatisticCountryWithDailyStatistic +import com.mobiledevpro.local.database.total.dao.BaseDao +import io.reactivex.Observable + +@Dao +interface StatisticCountryDataDao : BaseDao { + + @Query("SELECT * FROM countries_statistic") + fun getCountries(): Observable> + + @Transaction + @Query("SELECT * FROM countries_statistic WHERE country = :query") + fun getCountryByCountryName(query: String): Observable> +} \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/statistic/dao/StatisticDayCountryDataDao.kt b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/dao/StatisticDayCountryDataDao.kt new file mode 100644 index 0000000..12ca8b9 --- /dev/null +++ b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/dao/StatisticDayCountryDataDao.kt @@ -0,0 +1,14 @@ +package com.mobiledevpro.local.database.statistic.dao + +import androidx.room.Dao +import androidx.room.Query +import com.mobiledevpro.local.database.statistic.model.CachedDayTotalCountryStatistic +import com.mobiledevpro.local.database.total.dao.BaseDao +import io.reactivex.Observable + +@Dao +interface StatisticDayCountryDataDao : BaseDao { + + @Query("SELECT * FROM day_total_country_statistic") + fun getDayStatistics(): Observable> +} \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedDayTotalCountryStatistic.kt b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedDayTotalCountryStatistic.kt new file mode 100644 index 0000000..73d0212 --- /dev/null +++ b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedDayTotalCountryStatistic.kt @@ -0,0 +1,25 @@ +package com.mobiledevpro.local.database.statistic.model + +import androidx.room.Entity +import androidx.room.Index + +/** + * Data class for collect day statistic by country + * @property province is parent name of country/province + * @property date is a date of statistic + * @property confirmed is a confirmed count people + * @property deaths is a deaths of count people + * @property recovered is a recovered count people + * */ +@Entity( + tableName = "day_total_country_statistic", + indices = [Index("date")], + primaryKeys = ["date", "province"] +) +data class CachedDayTotalCountryStatistic( + val province: String, + val date: Long, + val confirmed: Long, + val deaths: Long, + val recovered: Long +) \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedStatisticCountry.kt b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedStatisticCountry.kt new file mode 100644 index 0000000..654030e --- /dev/null +++ b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedStatisticCountry.kt @@ -0,0 +1,23 @@ +package com.mobiledevpro.local.database.statistic.model + +import androidx.room.Entity +import androidx.room.Index + +/*** + * Data class for collect countries per static + * @property province is a name of province or state + * @property country is a name of country + * @property latitude is a latitude by coordinate + * @property longitude is a longitude by coordinate + */ +@Entity( + tableName = "countries_statistic", + primaryKeys = ["province"], + indices = [Index(value = ["province"], unique = false)] +) +data class CachedStatisticCountry( + val country: String, + val province: String, + val latitude: Double, + val longitude: Double +) \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedStatisticCountryWithDailyStatistic.kt b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedStatisticCountryWithDailyStatistic.kt new file mode 100644 index 0000000..40689c3 --- /dev/null +++ b/data-local/src/main/java/com/mobiledevpro/local/database/statistic/model/CachedStatisticCountryWithDailyStatistic.kt @@ -0,0 +1,16 @@ +package com.mobiledevpro.local.database.statistic.model + +import androidx.room.Embedded +import androidx.room.Relation + +class CachedStatisticCountryWithDailyStatistic { + + @Embedded + var country: CachedStatisticCountry? = null + + @Relation( + parentColumn = "province", + entityColumn = "province" + ) + var dayStatistics: List = listOf() +} \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/total/DefaultTotalCovidCache.kt b/data-local/src/main/java/com/mobiledevpro/local/database/total/DefaultTotalCovidCache.kt new file mode 100644 index 0000000..8b54d45 --- /dev/null +++ b/data-local/src/main/java/com/mobiledevpro/local/database/total/DefaultTotalCovidCache.kt @@ -0,0 +1,63 @@ +package com.mobiledevpro.local.database.total + +import com.mobiledevpro.data.model.CountryTotalEntity +import com.mobiledevpro.data.model.TotalEntity +import com.mobiledevpro.data.repository.userdata.TotalCovidCache +import com.mobiledevpro.local.database.AppDatabase +import com.mobiledevpro.local.database.total.model.CachedTotal +import com.mobiledevpro.local.database.total.model.CachedTotalCounties +import com.mobiledevpro.local.mapper.toCached +import com.mobiledevpro.local.mapper.toEntity +import io.reactivex.Completable +import io.reactivex.Observable + +/** + * Database Helper + * + * Created by Dmitriy Chernysh + * + * http://androiddev.pro + * + * #MobileDevPro + */ +class DefaultTotalCovidCache(private val database: AppDatabase) : TotalCovidCache { + + override fun getTotalDataObservable(): Observable = + database + .totalDataDao + .getTotalDataObservable() + .map(CachedTotal::toEntity) + + override fun updateTotalData(totalEntity: TotalEntity) = Completable + .create { emitter -> + val dao = database.totalDataDao + + dao.deleteAllTotalValues() + + dao.insert(totalEntity.toCached()) + + emitter.onComplete() + } + + override fun getLocalCountriesObservable(query: String): Observable> = + database + .totalCountryDataDao + .getCountiesDataObservable(query) + .map { it.map(CachedTotalCounties::toEntity) } + + override fun updateCountries(countriesTotalEntity: List) = Completable + .create { emitter -> + val dao = database.totalDataDao + + dao.deleteAllTotalValues() + + val countriesCached = countriesTotalEntity + .map(CountryTotalEntity::toCached) + + database + .totalCountryDataDao + .insert(countriesCached) + + emitter.onComplete() + } +} diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/dao/BaseDao.kt b/data-local/src/main/java/com/mobiledevpro/local/database/total/dao/BaseDao.kt similarity index 87% rename from data-local/src/main/java/com/mobiledevpro/local/database/dao/BaseDao.kt rename to data-local/src/main/java/com/mobiledevpro/local/database/total/dao/BaseDao.kt index d66338e..2d2800c 100644 --- a/data-local/src/main/java/com/mobiledevpro/local/database/dao/BaseDao.kt +++ b/data-local/src/main/java/com/mobiledevpro/local/database/total/dao/BaseDao.kt @@ -1,4 +1,4 @@ -package com.mobiledevpro.local.database.dao +package com.mobiledevpro.local.database.total.dao import androidx.room.Delete import androidx.room.Insert @@ -14,7 +14,7 @@ import androidx.room.Update * * http://androiddev.pro */ -internal interface BaseDao { +interface BaseDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(obj: T) diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/total/dao/TotalCountryDataDao.kt b/data-local/src/main/java/com/mobiledevpro/local/database/total/dao/TotalCountryDataDao.kt new file mode 100644 index 0000000..4ccf4a2 --- /dev/null +++ b/data-local/src/main/java/com/mobiledevpro/local/database/total/dao/TotalCountryDataDao.kt @@ -0,0 +1,16 @@ +package com.mobiledevpro.local.database.total.dao + +import androidx.room.Dao +import androidx.room.Query +import com.mobiledevpro.local.database.total.model.CachedTotalCounties +import io.reactivex.Observable + +@Dao +interface TotalCountryDataDao : BaseDao { + + @Query("SELECT * FROM counties_total WHERE country LIKE '%' || :query || '%' ORDER BY confirmed DESC") + fun getCountiesDataObservable(query: String): Observable> + + @Query("DELETE FROM counties_total") + fun deleteAllCountriesValues() +} \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/dao/TotalDataDao.kt b/data-local/src/main/java/com/mobiledevpro/local/database/total/dao/TotalDataDao.kt similarity index 60% rename from data-local/src/main/java/com/mobiledevpro/local/database/dao/TotalDataDao.kt rename to data-local/src/main/java/com/mobiledevpro/local/database/total/dao/TotalDataDao.kt index 946d74b..fd82f5b 100644 --- a/data-local/src/main/java/com/mobiledevpro/local/database/dao/TotalDataDao.kt +++ b/data-local/src/main/java/com/mobiledevpro/local/database/total/dao/TotalDataDao.kt @@ -1,17 +1,16 @@ -package com.mobiledevpro.local.database.dao +package com.mobiledevpro.local.database.total.dao import androidx.room.Dao import androidx.room.Query -import com.mobiledevpro.local.database.model.CachedTotal +import com.mobiledevpro.local.database.total.model.CachedTotal import io.reactivex.Observable @Dao -internal interface TotalDataDao : BaseDao { +interface TotalDataDao : BaseDao { @Query("SELECT * FROM total") fun getTotalDataObservable(): Observable @Query("DELETE FROM total") fun deleteAllTotalValues() - } \ No newline at end of file diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/model/CachedTotal.kt b/data-local/src/main/java/com/mobiledevpro/local/database/total/model/CachedTotal.kt similarity index 84% rename from data-local/src/main/java/com/mobiledevpro/local/database/model/CachedTotal.kt rename to data-local/src/main/java/com/mobiledevpro/local/database/total/model/CachedTotal.kt index 0885de0..0e42005 100644 --- a/data-local/src/main/java/com/mobiledevpro/local/database/model/CachedTotal.kt +++ b/data-local/src/main/java/com/mobiledevpro/local/database/total/model/CachedTotal.kt @@ -1,4 +1,4 @@ -package com.mobiledevpro.local.database.model +package com.mobiledevpro.local.database.total.model import androidx.room.Entity import androidx.room.PrimaryKey diff --git a/data-local/src/main/java/com/mobiledevpro/local/database/model/CachedCounties.kt b/data-local/src/main/java/com/mobiledevpro/local/database/total/model/CachedTotalCounties.kt similarity index 78% rename from data-local/src/main/java/com/mobiledevpro/local/database/model/CachedCounties.kt rename to data-local/src/main/java/com/mobiledevpro/local/database/total/model/CachedTotalCounties.kt index 3a8704a..0cf7d1f 100644 --- a/data-local/src/main/java/com/mobiledevpro/local/database/model/CachedCounties.kt +++ b/data-local/src/main/java/com/mobiledevpro/local/database/total/model/CachedTotalCounties.kt @@ -1,17 +1,17 @@ -package com.mobiledevpro.local.database.model +package com.mobiledevpro.local.database.total.model import androidx.room.Entity import androidx.room.Index @Entity( - tableName = "counties", + tableName = "counties_total", indices = [ Index(value = ["id"]), Index(value = ["country"]) ], primaryKeys = ["id"] ) -data class CachedCounties( +data class CachedTotalCounties( var id: Int = 0, diff --git a/data-local/src/main/java/com/mobiledevpro/local/mapper/MapperExtension.kt b/data-local/src/main/java/com/mobiledevpro/local/mapper/MapperExtension.kt index 065075c..fc6db45 100644 --- a/data-local/src/main/java/com/mobiledevpro/local/mapper/MapperExtension.kt +++ b/data-local/src/main/java/com/mobiledevpro/local/mapper/MapperExtension.kt @@ -1,9 +1,16 @@ package com.mobiledevpro.local.mapper -import com.mobiledevpro.data.model.CountryEntity +import com.mobiledevpro.data.model.CountryTotalEntity import com.mobiledevpro.data.model.TotalEntity -import com.mobiledevpro.local.database.model.CachedCounties -import com.mobiledevpro.local.database.model.CachedTotal +import com.mobiledevpro.data.model.statistic.CoordEntity +import com.mobiledevpro.data.model.statistic.CountyStatisticEntity +import com.mobiledevpro.data.model.statistic.DayStatisticEntity +import com.mobiledevpro.data.model.statistic.StatisticEntity +import com.mobiledevpro.local.database.statistic.model.CachedDayTotalCountryStatistic +import com.mobiledevpro.local.database.statistic.model.CachedStatisticCountry +import com.mobiledevpro.local.database.statistic.model.CachedStatisticCountryWithDailyStatistic +import com.mobiledevpro.local.database.total.model.CachedTotal +import com.mobiledevpro.local.database.total.model.CachedTotalCounties fun CachedTotal.toEntity() = TotalEntity( confirmed = confirmed, @@ -19,7 +26,7 @@ fun TotalEntity.toCached() = CachedTotal( lastUpdateTime = lastUpdateTime ) -fun CachedCounties.toEntity() = CountryEntity( +fun CachedTotalCounties.toEntity() = CountryTotalEntity( id = id, country = country, updated = updated, @@ -31,7 +38,7 @@ fun CachedCounties.toEntity() = CountryEntity( active = active ) -fun CountryEntity.toCached() = CachedCounties( +fun CountryTotalEntity.toCached() = CachedTotalCounties( id = id, country = country, updated = updated, @@ -41,4 +48,30 @@ fun CountryEntity.toCached() = CachedCounties( deaths = deaths, recovered = recovered, active = active +) + +fun StatisticEntity.toCache() = CachedStatisticCountry( + province = country.provinceName, + country = country.countryName, + latitude = coord.lat, + longitude = coord.long +) + +fun CachedStatisticCountryWithDailyStatistic.toEntity() = StatisticEntity( + country = CountyStatisticEntity( + countryName = country!!.country, + provinceName = country!!.province + ), + coord = CoordEntity( + lat = country!!.latitude, + long = country!!.longitude + ), + dayStatistic = dayStatistics.map { it.toEntity() } +) + +private fun CachedDayTotalCountryStatistic.toEntity() = DayStatisticEntity( + date = date, + confirmed = confirmed, + deaths = deaths, + recovered = recovered ) \ No newline at end of file diff --git a/data-remote/build.gradle b/data-remote/build.gradle index 522b48a..7cd0cc6 100644 --- a/data-remote/build.gradle +++ b/data-remote/build.gradle @@ -10,6 +10,9 @@ dependencies { implementation deps.retrofitRx implementation deps.okhttpLoggingInterceptor + //Parsing HTML + implementation(deps.jsoup) + //Firebase messaging implementation deps.fcm } diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultStatisticCovidRemote.kt b/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultStatisticCovidRemote.kt new file mode 100644 index 0000000..d95d301 --- /dev/null +++ b/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultStatisticCovidRemote.kt @@ -0,0 +1,19 @@ +package com.mobiledevpro.remote.implementation + +import com.mobiledevpro.data.repository.statistic.StatisticCovidRemote +import com.mobiledevpro.remote.mapper.toEntity +import com.mobiledevpro.remote.service.api.FullStatisticRestApiClient + +class DefaultStatisticCovidRemote( + private val apiStatistic: FullStatisticRestApiClient +) : StatisticCovidRemote { + + override fun getStatisticByPage(page: Int) = apiStatistic + .getStatistic(resultOffset = page) + .map { it.countries } + .map { + it.map { countryResponse -> + countryResponse.attribute.toEntity() + } + } +} \ No newline at end of file diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultStatisticsParserHtml.kt b/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultStatisticsParserHtml.kt new file mode 100644 index 0000000..f81501a --- /dev/null +++ b/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultStatisticsParserHtml.kt @@ -0,0 +1,134 @@ +package com.mobiledevpro.remote.implementation + +import com.mobiledevpro.data.model.HtmlParserThrowableEntity +import com.mobiledevpro.data.model.statistic.CoordEntity +import com.mobiledevpro.data.model.statistic.CountyStatisticEntity +import com.mobiledevpro.data.model.statistic.DayStatisticEntity +import com.mobiledevpro.data.model.statistic.StatisticEntity +import com.mobiledevpro.data.repository.parcer.StatisticsParserHtml +import com.mobiledevpro.remote.mapper.toDateLong +import io.reactivex.Single +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.select.Elements + +class DefaultStatisticsParserHtml : StatisticsParserHtml { + + override fun getConfirmedStatistics(): Single> = + Single.fromCallable { getDataStatistic(CONFIRMED_FILE_NAME) } + + override fun getDeathsStatistics(): Single> = + Single.fromCallable { getDataStatistic(DEATHS_FILE_NAME) } + + override fun getRecoveredStatistics(): Single> = + Single.fromCallable { getDataStatistic(RECOVERED_FILE_NAME) } + + + private fun getDataStatistic(fileName: String): ArrayList { + + val counties = ArrayList() + + try { + val docHtml: Document = Jsoup.connect("$BASE_URL$fileName").get() + + val titlesHtml = docHtml + .getElementById("${LINE_IDENTIFICATION}1") + .parent() + .getElementsByTag(LINE_SEPARATOR) + + val countCountries = docHtml + .getElementsByTag(ROW_IDENTIFICATION) + .size + + for (i in 2 until countCountries) { + + val countryHtml = docHtml + .getElementById("$LINE_IDENTIFICATION${i}") + .parent() + .getElementsByTag(ROW_SEPARATOR) + + val countryName = countryHtml.toStringValue(2) + + var provinceName = countryHtml.toStringValue(1).apply { + when (this.isEmpty()) { + true -> countryName + else -> this + } + } + + if (provinceName.isEmpty()) provinceName = countryName + + val countyStatisticEntity = CountyStatisticEntity( + provinceName = provinceName, + countryName = countryName + ) + + val coordEntity = CoordEntity( + lat = countryHtml.toDoubleValue(3), + long = countryHtml.toDoubleValue(4) + ) + + val dayCountsEntity = ArrayList() + + for (j in 5 until countryHtml.size) { + dayCountsEntity.add( + DayStatisticEntity( + date = titlesHtml.toStringValue(j - 1).toDateLong(), + confirmed = if (fileName == CONFIRMED_FILE_NAME) countryHtml.toLongValue(j) else 0L, + deaths = if (fileName == DEATHS_FILE_NAME) countryHtml.toLongValue(j) else 0L, + recovered = if (fileName == RECOVERED_FILE_NAME) countryHtml.toLongValue(j) else 0L + ) + ) + } + + counties.add( + StatisticEntity( + country = countyStatisticEntity, + coord = coordEntity, + dayStatistic = dayCountsEntity + ) + ) + } + } catch (e: Exception) { + throw HtmlParserThrowableEntity( + "Parser Error: ${e.message}" + ) + } + + return counties + } + + private fun Elements.toStringValue(index: Int): String = + if (this[index].childNodes().isEmpty()) "" + else this[index].childNodes().first().toString() + + private fun Elements.toDoubleValue(index: Int): Double = + if (this[index].childNodes().isEmpty()) 0.0 + else this[index].childNodes().first().toString().toDouble() + + private fun Elements.toLongValue(index: Int): Long = + if (this[index].childNodes().isEmpty()) 0L + else this[index].childNodes().first().toString().toLong() + + private companion object { + + const val BASE_URL = +// "https://github.com/CSSEGISandData/COVID-19/tree/master/csse_covid_19_data/csse_covid_19_time_series/" + + "https://github.com/CSSEGISandData/COVID-19/blob/master/csse_covid_19_data/csse_covid_19_time_series/" + + const val CONFIRMED_FILE_NAME = "time_series_covid19_confirmed_global.csv" + + const val DEATHS_FILE_NAME = "time_series_covid19_deaths_global.csv" + + const val RECOVERED_FILE_NAME = "time_series_covid19_recovered_global.csv" + + const val LINE_IDENTIFICATION = "L" + + const val ROW_IDENTIFICATION = "tr" + + const val LINE_SEPARATOR = "th" + + const val ROW_SEPARATOR = "td" + } +} diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultTotalDataIRemote.kt b/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultTotalCovidRemote.kt similarity index 53% rename from data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultTotalDataIRemote.kt rename to data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultTotalCovidRemote.kt index 98e7094..c3faa82 100644 --- a/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultTotalDataIRemote.kt +++ b/data-remote/src/main/java/com/mobiledevpro/remote/implementation/DefaultTotalCovidRemote.kt @@ -1,30 +1,29 @@ package com.mobiledevpro.remote.implementation -import com.mobiledevpro.data.model.CountryEntity -import com.mobiledevpro.data.repository.userdata.CovidRemote +import com.mobiledevpro.data.model.CountryTotalEntity +import com.mobiledevpro.data.repository.userdata.TotalCovidRemote import com.mobiledevpro.remote.mapper.toEntity -import com.mobiledevpro.remote.model.response.CountryResponse import com.mobiledevpro.remote.model.response.TotalResponse -import com.mobiledevpro.remote.service.api.RestApiClient +import com.mobiledevpro.remote.service.api.TotalRestApiClient import io.reactivex.Single -class DefaultTotalDataIRemote( - private val api: RestApiClient -) : CovidRemote { +class DefaultTotalCovidRemote( + private val apiTotal: TotalRestApiClient +) : TotalCovidRemote { - override fun getTotalConfirmed() = api + override fun getTotalConfirmed() = apiTotal .getTotalConfirmed() .map(TotalResponse::toEntity) - override fun getTotalDeaths() = api + override fun getTotalDeaths() = apiTotal .getTotalDeaths() .map(TotalResponse::toEntity) - override fun getTotalRecovered() = api + override fun getTotalRecovered() = apiTotal .getTotalRecovered() .map(TotalResponse::toEntity) - override fun getCountries(): Single> = api + override fun getCountries(): Single> = apiTotal .getCountries() .map { it.countries } .map { diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/mapper/MapperExtension.kt b/data-remote/src/main/java/com/mobiledevpro/remote/mapper/MapperExtension.kt index b87c57e..1d91369 100644 --- a/data-remote/src/main/java/com/mobiledevpro/remote/mapper/MapperExtension.kt +++ b/data-remote/src/main/java/com/mobiledevpro/remote/mapper/MapperExtension.kt @@ -1,15 +1,19 @@ package com.mobiledevpro.remote.mapper -import com.mobiledevpro.data.model.CountryEntity +import com.mobiledevpro.data.model.CountryTotalEntity import com.mobiledevpro.data.model.TotalValueEntity -import com.mobiledevpro.remote.model.response.CountryResponse +import com.mobiledevpro.data.model.statistic.CountryStatisticEntity +import com.mobiledevpro.remote.model.response.CountryStatisticResponse +import com.mobiledevpro.remote.model.response.CountryTotalResponse import com.mobiledevpro.remote.model.response.TotalResponse +import java.text.SimpleDateFormat +import java.util.* fun TotalResponse.toEntity() = TotalValueEntity( - count = feature?.first()?.attribute?.value ?: -1 + count = feature.first().attribute.value ) -fun CountryResponse.toEntity() = CountryEntity( +fun CountryTotalResponse.toEntity() = CountryTotalEntity( id = id, country = country, updated = updated, @@ -19,4 +23,19 @@ fun CountryResponse.toEntity() = CountryEntity( deaths = deaths, recovered = recovered, active = active -) \ No newline at end of file +) + +fun CountryStatisticResponse.toEntity() = CountryStatisticEntity( + id = id, + country = country, + updated = updated, + confirmed = confirmed, + deaths = deaths, + deltaConfirmed = deltaConfirmed +) + +fun String.toDateLong(): Long { + val dateFormat = SimpleDateFormat("MM/dd/yy", Locale.getDefault()) + val date = dateFormat.parse(this) + return date.time +} \ No newline at end of file diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/model/response/CountriesResponse.kt b/data-remote/src/main/java/com/mobiledevpro/remote/model/response/CountriesTotalResponse.kt similarity index 67% rename from data-remote/src/main/java/com/mobiledevpro/remote/model/response/CountriesResponse.kt rename to data-remote/src/main/java/com/mobiledevpro/remote/model/response/CountriesTotalResponse.kt index 20c1752..78bdd36 100644 --- a/data-remote/src/main/java/com/mobiledevpro/remote/model/response/CountriesResponse.kt +++ b/data-remote/src/main/java/com/mobiledevpro/remote/model/response/CountriesTotalResponse.kt @@ -2,15 +2,15 @@ package com.mobiledevpro.remote.model.response import com.google.gson.annotations.SerializedName -data class CountriesResponse( - @SerializedName("features") val countries: List +data class CountriesTotalResponse( + @SerializedName("features") val countries: List ) -data class CountriesAttributeResponse( - @SerializedName("attributes") val attribute: CountryResponse +data class CountriesTotalAttributeResponse( + @SerializedName("attributes") val attribute: CountryTotalResponse ) -data class CountryResponse( +data class CountryTotalResponse( @SerializedName("OBJECTID") val id: Int, @SerializedName("Country_Region") val country: String, @SerializedName("Last_Update") val updated: Long, diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/model/response/StatisticResponse.kt b/data-remote/src/main/java/com/mobiledevpro/remote/model/response/StatisticResponse.kt new file mode 100644 index 0000000..a91cf9a --- /dev/null +++ b/data-remote/src/main/java/com/mobiledevpro/remote/model/response/StatisticResponse.kt @@ -0,0 +1,20 @@ +package com.mobiledevpro.remote.model.response + +import com.google.gson.annotations.SerializedName + +data class CountiesStatisticResponse( + @SerializedName("features") val countries: List +) + +data class CountriesStatisticAttributeResponse( + @SerializedName("attributes") val attribute: CountryStatisticResponse +) + +data class CountryStatisticResponse( + @SerializedName("OBJECTID") val id: Int, + @SerializedName("Country_Region") val country: String, + @SerializedName("Last_Update") val updated: Long, + @SerializedName("Confirmed") val confirmed: Long, + @SerializedName("Deaths") val deaths: Long, + @SerializedName("Delta_Confirmed") val deltaConfirmed: Long +) diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/model/response/TotalResponse.kt b/data-remote/src/main/java/com/mobiledevpro/remote/model/response/TotalResponse.kt index d446ec3..004cb0f 100644 --- a/data-remote/src/main/java/com/mobiledevpro/remote/model/response/TotalResponse.kt +++ b/data-remote/src/main/java/com/mobiledevpro/remote/model/response/TotalResponse.kt @@ -3,13 +3,13 @@ package com.mobiledevpro.remote.model.response import com.google.gson.annotations.SerializedName data class TotalResponse( - @SerializedName("features") val feature: List? + @SerializedName("features") val feature: List ) data class TotalAttributeResponse( - @SerializedName("attributes") val attribute: ValueResponse? + @SerializedName("attributes") val attribute: ValueResponse ) data class ValueResponse( - @SerializedName("value") val value: Int = -1 + @SerializedName("value") val value: Int ) \ No newline at end of file diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/service/RemoteServiceFactory.kt b/data-remote/src/main/java/com/mobiledevpro/remote/service/RemoteServiceFactory.kt index ae98e30..32e95bb 100644 --- a/data-remote/src/main/java/com/mobiledevpro/remote/service/RemoteServiceFactory.kt +++ b/data-remote/src/main/java/com/mobiledevpro/remote/service/RemoteServiceFactory.kt @@ -1,7 +1,8 @@ package com.mobiledevpro.remote.service import com.google.gson.GsonBuilder -import com.mobiledevpro.remote.service.api.RestApiClient +import com.mobiledevpro.remote.service.api.FullStatisticRestApiClient +import com.mobiledevpro.remote.service.api.TotalRestApiClient import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory @@ -11,10 +12,15 @@ class RemoteServiceFactory( client: OkHttpClient ) { - fun buildStackOverFlowApi(): RestApiClient = builder - .baseUrl(BASE_URL) + fun buildCovidTotalApi(): TotalRestApiClient = builder + .baseUrl(BASE_DAILY_URL) .build() - .create(RestApiClient::class.java) + .create(TotalRestApiClient::class.java) + + fun buildCovidFullStatisticsApi(): FullStatisticRestApiClient = builder + .baseUrl(BASE_FULL_STATISTICS_URL) + .build() + .create(FullStatisticRestApiClient::class.java) private val gson = GsonBuilder() .setLenient() @@ -26,7 +32,11 @@ class RemoteServiceFactory( .addConverterFactory(GsonConverterFactory.create(gson)) companion object { - private const val BASE_URL = + + private const val BASE_DAILY_URL = "https://services1.arcgis.com/0MSEUqKaxRlEPj5g/arcgis/rest/services/ncov_cases/FeatureServer/2/query/" + + private const val BASE_FULL_STATISTICS_URL = + "https://services9.arcgis.com/N9p5hsImWXAccRNI/arcgis/rest/services/Nc2JKvYFoAEOFCG5JSI6/FeatureServer/4/query/" } -} \ No newline at end of file +} diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/service/api/FullStatisticRestApiClient.kt b/data-remote/src/main/java/com/mobiledevpro/remote/service/api/FullStatisticRestApiClient.kt new file mode 100644 index 0000000..a62fb9d --- /dev/null +++ b/data-remote/src/main/java/com/mobiledevpro/remote/service/api/FullStatisticRestApiClient.kt @@ -0,0 +1,25 @@ +package com.mobiledevpro.remote.service.api + +import com.mobiledevpro.remote.model.response.CountiesStatisticResponse +import io.reactivex.Single +import retrofit2.http.GET +import retrofit2.http.Query + +interface FullStatisticRestApiClient { + + @GET(".") + fun getStatistic( + @Query(value = "f", encoded = true) format: String = "json", + @Query(value = "where", encoded = true) where: String = "Confirmed>0", + @Query(value = "returnGeometry", encoded = true) geometry: Boolean = false, + @Query( + value = "spatialRel", + encoded = true + ) spatialRel: String = "esriSpatialRelIntersects", + @Query(value = "outFields", encoded = true) outFields: String = "*", + @Query(value = "orderByFields", encoded = true) orderedByFields: String = "Last_Update%20asc", + @Query(value = "resultOffset", encoded = true) resultOffset: Int = 0, + @Query(value = "resultRecordCount", encoded = true) resultRecordCount: Int = 1000, + @Query(value = "cacheHint", encoded = true) cacheHint: Boolean = true + ): Single +} \ No newline at end of file diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/service/api/RestApiClient.kt b/data-remote/src/main/java/com/mobiledevpro/remote/service/api/TotalRestApiClient.kt similarity index 96% rename from data-remote/src/main/java/com/mobiledevpro/remote/service/api/RestApiClient.kt rename to data-remote/src/main/java/com/mobiledevpro/remote/service/api/TotalRestApiClient.kt index 5c8fd36..7d4a167 100644 --- a/data-remote/src/main/java/com/mobiledevpro/remote/service/api/RestApiClient.kt +++ b/data-remote/src/main/java/com/mobiledevpro/remote/service/api/TotalRestApiClient.kt @@ -1,6 +1,6 @@ package com.mobiledevpro.remote.service.api -import com.mobiledevpro.remote.model.response.CountriesResponse +import com.mobiledevpro.remote.model.response.CountriesTotalResponse import com.mobiledevpro.remote.model.response.TotalResponse import io.reactivex.Single import retrofit2.http.GET @@ -16,7 +16,7 @@ import retrofit2.http.Query * https://instagr.am/mobiledevpro * #MobileDevPro */ -interface RestApiClient { +interface TotalRestApiClient { @GET(".") fun getTotalConfirmed( @@ -87,6 +87,6 @@ interface RestApiClient { @Query(value = "resultOffset", encoded = true) resultOffset: Int = 0, @Query(value = "resultRecordCount", encoded = true) resultRecordCount: Int = 1000, @Query(value = "cacheHint", encoded = true) cacheHint: Boolean = true - ): Single + ): Single } \ No newline at end of file diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/service/interceptor/ApiRequestInterceptor.kt b/data-remote/src/main/java/com/mobiledevpro/remote/service/interceptor/ApiRequestInterceptor.kt deleted file mode 100644 index 4efeb79..0000000 --- a/data-remote/src/main/java/com/mobiledevpro/remote/service/interceptor/ApiRequestInterceptor.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.mobiledevpro.remote.service.interceptor - -import okhttp3.Interceptor -import okhttp3.Response - -class ApiRequestInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - - val builder = chain.request().newBuilder().apply { - // There you can add Headers - } - - return chain.proceed(builder.build()) - } -} \ No newline at end of file diff --git a/data-remote/src/main/java/com/mobiledevpro/remote/service/interceptor/ApiStatisticsRequestInterceptor.kt b/data-remote/src/main/java/com/mobiledevpro/remote/service/interceptor/ApiStatisticsRequestInterceptor.kt new file mode 100644 index 0000000..1e8e5ad --- /dev/null +++ b/data-remote/src/main/java/com/mobiledevpro/remote/service/interceptor/ApiStatisticsRequestInterceptor.kt @@ -0,0 +1,32 @@ +package com.mobiledevpro.remote.service.interceptor + +import okhttp3.Interceptor +import okhttp3.Response + +class ApiStatisticsRequestInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response = chain.run { + proceed( + request() + .newBuilder() + .addHeader( + name = REFERER_NAME, + value = REFERER_VALUE + ) + .removeHeader(USER_AGENT_NAME) + .addHeader( + name = USER_AGENT_NAME, + value = USER_AGENT_VALUE + ) + .build() + ) + } + + private companion object { + const val USER_AGENT_NAME = "User-Agent" + const val USER_AGENT_VALUE = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" + + const val REFERER_NAME = "Referer" + const val REFERER_VALUE = "https://www.arcgis.com/apps/opsdashboard/index.html" + } +} diff --git a/data/src/main/java/com/mobiledevpro/data/mapper/MapperExtension.kt b/data/src/main/java/com/mobiledevpro/data/mapper/MapperExtension.kt index 1f9b452..562aa6e 100644 --- a/data/src/main/java/com/mobiledevpro/data/mapper/MapperExtension.kt +++ b/data/src/main/java/com/mobiledevpro/data/mapper/MapperExtension.kt @@ -1,19 +1,14 @@ package com.mobiledevpro.data.mapper -import com.mobiledevpro.data.model.AccessDeniedThrowableEntity -import com.mobiledevpro.data.model.CountryEntity -import com.mobiledevpro.data.model.NetworkThrowableEntity -import com.mobiledevpro.data.model.NotFoundThrowableEntity -import com.mobiledevpro.data.model.ServiceUnavailableThrowableEntity -import com.mobiledevpro.data.model.TotalEntity -import com.mobiledevpro.data.model.UnknownThrowableEntity -import com.mobiledevpro.domain.error.AccessDeniedThrowable -import com.mobiledevpro.domain.error.NetworkThrowable -import com.mobiledevpro.domain.error.NotFoundThrowable -import com.mobiledevpro.domain.error.ServiceUnavailableThrowable -import com.mobiledevpro.domain.error.UnknownThrowable -import com.mobiledevpro.domain.model.Country +import com.mobiledevpro.data.model.* +import com.mobiledevpro.data.model.statistic.CountryStatisticEntity +import com.mobiledevpro.data.model.statistic.DayStatisticEntity +import com.mobiledevpro.data.model.statistic.StatisticEntity +import com.mobiledevpro.domain.error.* +import com.mobiledevpro.domain.model.DayStatistic +import com.mobiledevpro.domain.model.StatisticCountry import com.mobiledevpro.domain.model.Total +import com.mobiledevpro.domain.model.TotalCountry import java.util.* /** @@ -39,7 +34,7 @@ fun TotalEntity.toDomain() = Total( updateTime = lastUpdateTime ) -fun CountryEntity.toDomain() = Country( +fun CountryTotalEntity.toDomain() = TotalCountry( id = id, country = country, updated = updated, @@ -51,7 +46,7 @@ fun CountryEntity.toDomain() = Country( active = active ) -fun Country.toEntity() = CountryEntity( +fun TotalCountry.toEntity() = CountryTotalEntity( id = id, country = country, updated = updated, @@ -63,11 +58,35 @@ fun Country.toEntity() = CountryEntity( active = active ) +fun CountryStatisticEntity.toDomain() = TotalCountry( + id = id, + country = country, + updated = updated, + confirmed = confirmed.toInt(), + deaths = deaths.toInt() +) + +fun StatisticEntity.toDomain() = StatisticCountry( + country = country.countryName, + province = country.provinceName, + latitude = coord.lat, + longitude = coord.long, + dayStatistics = dayStatistic.map { it.toDomain() } +) + +fun DayStatisticEntity.toDomain() = DayStatistic( + date = date, + totalConfirmed = confirmed, + totalRecovered = recovered, + totalDeaths = deaths +) + fun Throwable.throwableToDomain() = when (this) { is NetworkThrowableEntity -> NetworkThrowable() is NotFoundThrowableEntity -> NotFoundThrowable() is AccessDeniedThrowableEntity -> AccessDeniedThrowable() is ServiceUnavailableThrowableEntity -> ServiceUnavailableThrowable() + is HtmlParserThrowableEntity -> HtmlParserThrowable(message = message) is UnknownThrowableEntity -> UnknownThrowable() else -> UnknownThrowable() } \ No newline at end of file diff --git a/data/src/main/java/com/mobiledevpro/data/model/CountryEntity.kt b/data/src/main/java/com/mobiledevpro/data/model/CountryTotalEntity.kt similarity index 88% rename from data/src/main/java/com/mobiledevpro/data/model/CountryEntity.kt rename to data/src/main/java/com/mobiledevpro/data/model/CountryTotalEntity.kt index 0e4867a..423350c 100644 --- a/data/src/main/java/com/mobiledevpro/data/model/CountryEntity.kt +++ b/data/src/main/java/com/mobiledevpro/data/model/CountryTotalEntity.kt @@ -1,6 +1,6 @@ package com.mobiledevpro.data.model -data class CountryEntity( +data class CountryTotalEntity( val id: Int, val country: String, val updated: Long, diff --git a/data/src/main/java/com/mobiledevpro/data/model/ErrorEntity.kt b/data/src/main/java/com/mobiledevpro/data/model/ErrorEntity.kt index b7bc68d..2e148a4 100644 --- a/data/src/main/java/com/mobiledevpro/data/model/ErrorEntity.kt +++ b/data/src/main/java/com/mobiledevpro/data/model/ErrorEntity.kt @@ -9,3 +9,5 @@ class AccessDeniedThrowableEntity : Throwable() class ServiceUnavailableThrowableEntity : Throwable() class UnknownThrowableEntity : Throwable() + +class HtmlParserThrowableEntity(override val message: String?) : Throwable() diff --git a/data/src/main/java/com/mobiledevpro/data/model/statistic/CountryStatisticEntity.kt b/data/src/main/java/com/mobiledevpro/data/model/statistic/CountryStatisticEntity.kt new file mode 100644 index 0000000..cde839e --- /dev/null +++ b/data/src/main/java/com/mobiledevpro/data/model/statistic/CountryStatisticEntity.kt @@ -0,0 +1,10 @@ +package com.mobiledevpro.data.model.statistic + +data class CountryStatisticEntity( + val id: Int, + val country: String, + val updated: Long, + val confirmed: Long, + val deaths: Long, + val deltaConfirmed: Long +) \ No newline at end of file diff --git a/data/src/main/java/com/mobiledevpro/data/model/statistic/StatisticEntity.kt b/data/src/main/java/com/mobiledevpro/data/model/statistic/StatisticEntity.kt new file mode 100644 index 0000000..eb5cb44 --- /dev/null +++ b/data/src/main/java/com/mobiledevpro/data/model/statistic/StatisticEntity.kt @@ -0,0 +1,47 @@ +package com.mobiledevpro.data.model.statistic + +/** + * Data class describe statistic by country with dates and count of people + * @property country is a country with descriptions + * @property coord is a coordinates with a latitude and longitude + * @property dayStatistic is a list of people counts by date + */ +data class StatisticEntity( + val country: CountyStatisticEntity, + val coord: CoordEntity, + val dayStatistic: List +) + +/** + * Data class describe country data + * @property provinceName is a Province/State + * @property countryName is a Country/Region + */ +data class CountyStatisticEntity( + val provinceName: String, + val countryName: String +) + +/** + * Data class for location coordinates + * @property lat is a latitude + * @property long is a longitude + */ +data class CoordEntity( + val lat: Double, + val long: Double +) + +/** + * Data class for day people total count by date + * @property date is a date in format like 22/03/20 + * @property confirmed is a confirmed count people + * @property deaths is a deaths of count people + * @property recovered is a recovered count people + */ +data class DayStatisticEntity( + val date: Long, + var confirmed: Long = 0L, + var deaths: Long = 0L, + var recovered: Long = 0 +) diff --git a/data/src/main/java/com/mobiledevpro/data/repository/parcer/StatisticsParserHtml.kt b/data/src/main/java/com/mobiledevpro/data/repository/parcer/StatisticsParserHtml.kt new file mode 100644 index 0000000..0cad0c7 --- /dev/null +++ b/data/src/main/java/com/mobiledevpro/data/repository/parcer/StatisticsParserHtml.kt @@ -0,0 +1,13 @@ +package com.mobiledevpro.data.repository.parcer + +import com.mobiledevpro.data.model.statistic.StatisticEntity +import io.reactivex.Single + +interface StatisticsParserHtml { + + fun getConfirmedStatistics(): Single> + + fun getDeathsStatistics(): Single> + + fun getRecoveredStatistics(): Single> +} \ No newline at end of file diff --git a/data/src/main/java/com/mobiledevpro/data/repository/statistic/DefaultStatisticDataRepository.kt b/data/src/main/java/com/mobiledevpro/data/repository/statistic/DefaultStatisticDataRepository.kt new file mode 100644 index 0000000..4caec71 --- /dev/null +++ b/data/src/main/java/com/mobiledevpro/data/repository/statistic/DefaultStatisticDataRepository.kt @@ -0,0 +1,130 @@ +package com.mobiledevpro.data.repository.statistic + +import com.mobiledevpro.data.mapper.throwableToDomain +import com.mobiledevpro.data.mapper.toDomain +import com.mobiledevpro.data.model.statistic.CoordEntity +import com.mobiledevpro.data.model.statistic.CountryStatisticEntity +import com.mobiledevpro.data.model.statistic.CountyStatisticEntity +import com.mobiledevpro.data.model.statistic.DayStatisticEntity +import com.mobiledevpro.data.model.statistic.StatisticEntity +import com.mobiledevpro.data.repository.parcer.StatisticsParserHtml +import com.mobiledevpro.domain.model.StatisticCountry +import com.mobiledevpro.domain.statistic.data.StatisticDataRepository +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.functions.Function3 + +class DefaultStatisticDataRepository( + private val statisticsCache: StatisticCovidCache, + private val statisticRemote: StatisticCovidRemote, + private val statisticsParserHtml: StatisticsParserHtml +) : StatisticDataRepository { + + override fun getStatisticFromApiByPage(page: Int) = statisticRemote + .getStatisticByPage(page) + .map { it.map(CountryStatisticEntity::toDomain) } + + override fun fetchStatisticsFromHtml() = Single + .zip, ArrayList, ArrayList, ArrayList>( + statisticsParserHtml.getConfirmedStatistics(), + statisticsParserHtml.getDeathsStatistics(), + statisticsParserHtml.getRecoveredStatistics(), + Function3 { confirmed, deaths, recovered -> + val convertedStatisticCountry = ArrayList() + + for (i in confirmed.indices) { + val dayStatisticsEntity = ArrayList() + + for (j in confirmed[i].dayStatistic.indices) { + dayStatisticsEntity.add( + DayStatisticEntity( + date = confirmed[i].dayStatistic[j].date, + confirmed = confirmed[i].dayStatistic[j].confirmed + ) + ) + } + + convertedStatisticCountry.add( + StatisticEntity( + CountyStatisticEntity( + provinceName = confirmed[i].country.provinceName, + countryName = confirmed[i].country.countryName + ), + CoordEntity( + lat = confirmed[i].coord.lat, + long = confirmed[i].coord.long + ), + dayStatistic = dayStatisticsEntity + ) + ) + } + + for (i in deaths.indices) + convertedStatisticCountry.filter { it.country.provinceName == deaths[i].country.provinceName } + .map { country -> + for (j in deaths[i].dayStatistic.indices) + country.dayStatistic + .filter { it.date == deaths[i].dayStatistic[j].date } + .map { it.deaths = deaths[i].dayStatistic[j].deaths } + } + + for (i in recovered.indices) + convertedStatisticCountry.filter { it.country.provinceName == recovered[i].country.provinceName } + .map { country -> + for (j in recovered[i].dayStatistic.indices) + country.dayStatistic + .filter { it.date == recovered[i].dayStatistic[j].date } + .map { it.recovered = recovered[i].dayStatistic[j].recovered } + } + + convertedStatisticCountry + } + ) + .flatMapCompletable(statisticsCache::updateConfirmedData) + .throwableToDomain() + + override fun observeStatisticByCountyName(query: String): Observable = + statisticsCache + .observeConfirmedDataByCountryName(query) + .map { result -> + if (result.size <= 1) result[0] + else collectDataByDay(result) + } + .map(StatisticEntity::toDomain) + .throwableToDomain() + + private fun collectDataByDay(result: List): StatisticEntity { + val convertedDaysTotalEntity = ArrayList() + + for (i in result[0].dayStatistic.indices) { + var confirmed = 0L + var deaths = 0L + var recovered = 0L + + for (j in result.indices) { + confirmed += result[j].dayStatistic[i].confirmed + deaths += result[j].dayStatistic[i].deaths + recovered += result[j].dayStatistic[i].recovered + } + + convertedDaysTotalEntity.add( + DayStatisticEntity( + date = result[0].dayStatistic[i].date, + confirmed = confirmed, + deaths = deaths, + recovered = recovered + ) + ) + } + + return StatisticEntity( + country = CountyStatisticEntity( + countryName = result[0].country.countryName, + provinceName = result[0].country.countryName + ), + coord = result[0].coord, + dayStatistic = convertedDaysTotalEntity + ) + + } +} \ No newline at end of file diff --git a/data/src/main/java/com/mobiledevpro/data/repository/statistic/StatisticCovidCache.kt b/data/src/main/java/com/mobiledevpro/data/repository/statistic/StatisticCovidCache.kt new file mode 100644 index 0000000..6042b84 --- /dev/null +++ b/data/src/main/java/com/mobiledevpro/data/repository/statistic/StatisticCovidCache.kt @@ -0,0 +1,12 @@ +package com.mobiledevpro.data.repository.statistic + +import com.mobiledevpro.data.model.statistic.StatisticEntity +import io.reactivex.Completable +import io.reactivex.Observable + +interface StatisticCovidCache { + + fun updateConfirmedData(statistics: ArrayList): Completable + + fun observeConfirmedDataByCountryName(query: String): Observable> +} \ No newline at end of file diff --git a/data/src/main/java/com/mobiledevpro/data/repository/statistic/StatisticCovidRemote.kt b/data/src/main/java/com/mobiledevpro/data/repository/statistic/StatisticCovidRemote.kt new file mode 100644 index 0000000..78914e5 --- /dev/null +++ b/data/src/main/java/com/mobiledevpro/data/repository/statistic/StatisticCovidRemote.kt @@ -0,0 +1,9 @@ +package com.mobiledevpro.data.repository.statistic + +import com.mobiledevpro.data.model.statistic.CountryStatisticEntity +import io.reactivex.Single + +interface StatisticCovidRemote { + + fun getStatisticByPage(page: Int): Single> +} \ No newline at end of file diff --git a/data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidFactory.kt b/data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidFactory.kt index 77633e4..d6e7947 100644 --- a/data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidFactory.kt +++ b/data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidFactory.kt @@ -1,11 +1,11 @@ package com.mobiledevpro.data.repository.userdata class CovidFactory( - private val covidCache: CovidCache, - private val covidRemote: CovidRemote + private val totalCovidCache: TotalCovidCache, + private val totalCovidRemote: TotalCovidRemote ) { - fun remote() = covidRemote + fun remote() = totalCovidRemote - fun cache() = covidCache + fun cache() = totalCovidCache } \ No newline at end of file diff --git a/data/src/main/java/com/mobiledevpro/data/repository/userdata/DefaultTotalDataRepository.kt b/data/src/main/java/com/mobiledevpro/data/repository/userdata/DefaultTotalDataRepository.kt index 3ae8c48..c5ca826 100644 --- a/data/src/main/java/com/mobiledevpro/data/repository/userdata/DefaultTotalDataRepository.kt +++ b/data/src/main/java/com/mobiledevpro/data/repository/userdata/DefaultTotalDataRepository.kt @@ -4,11 +4,11 @@ import com.mobiledevpro.data.mapper.throwableToDomain import com.mobiledevpro.data.mapper.toCacheEntity import com.mobiledevpro.data.mapper.toDomain import com.mobiledevpro.data.mapper.toEntity -import com.mobiledevpro.data.model.CountryEntity +import com.mobiledevpro.data.model.CountryTotalEntity import com.mobiledevpro.data.model.TotalEntity import com.mobiledevpro.data.model.TotalValueEntity -import com.mobiledevpro.domain.model.Country import com.mobiledevpro.domain.model.Total +import com.mobiledevpro.domain.model.TotalCountry import com.mobiledevpro.domain.totaldata.TotalDataRepository import io.reactivex.Completable import io.reactivex.Observable @@ -26,11 +26,11 @@ import io.reactivex.functions.Function3 * #MobileDevPro */ class DefaultTotalDataRepository( - private val covidCache: CovidCache, - private val covidRemote: CovidRemote + private val totalCovidCache: TotalCovidCache, + private val totalCovidRemote: TotalCovidRemote ) : TotalDataRepository { - override fun getLocalTotalDataObservable(): Observable = covidCache + override fun getLocalTotalDataObservable(): Observable = totalCovidCache .getTotalDataObservable() .map(TotalEntity::toDomain) .throwableToDomain() @@ -38,14 +38,14 @@ class DefaultTotalDataRepository( override fun setLocalTotalData(total: Total): Completable = Single .just(total) .map(Total::toCacheEntity) - .flatMapCompletable(covidCache::updateTotalData) + .flatMapCompletable(totalCovidCache::updateTotalData) .throwableToDomain() override fun getTotalData(): Single = Single .zip( - covidRemote.getTotalConfirmed(), - covidRemote.getTotalDeaths(), - covidRemote.getTotalRecovered(), + totalCovidRemote.getTotalConfirmed(), + totalCovidRemote.getTotalDeaths(), + totalCovidRemote.getTotalRecovered(), Function3 { countConfirmed, countDeaths, countRecovered -> Total( confirmed = countConfirmed.count, @@ -56,20 +56,20 @@ class DefaultTotalDataRepository( .throwableToDomain() - override fun getLocalCountriesObservable(query: String): Observable> = covidCache + override fun getLocalCountriesObservable(query: String): Observable> = totalCovidCache .getLocalCountriesObservable(query) - .map { it.map(CountryEntity::toDomain) } + .map { it.map(CountryTotalEntity::toDomain) } .throwableToDomain() - override fun getCountries(): Single> = covidRemote + override fun getCountries(): Single> = totalCovidRemote .getCountries() - .map { it.map(CountryEntity::toDomain) } + .map { it.map(CountryTotalEntity::toDomain) } .throwableToDomain() - override fun setLocalCountriesData(countries: List): Completable = Single - .just(countries) - .map { it.map(Country::toEntity) } - .flatMapCompletable(covidCache::updateCountries) + override fun setLocalCountriesData(totalCountries: List): Completable = Single + .just(totalCountries) + .map { it.map(TotalCountry::toEntity) } + .flatMapCompletable(totalCovidCache::updateCountries) .throwableToDomain() } diff --git a/data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidCache.kt b/data/src/main/java/com/mobiledevpro/data/repository/userdata/TotalCovidCache.kt similarity index 62% rename from data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidCache.kt rename to data/src/main/java/com/mobiledevpro/data/repository/userdata/TotalCovidCache.kt index a517d9d..db07d6a 100644 --- a/data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidCache.kt +++ b/data/src/main/java/com/mobiledevpro/data/repository/userdata/TotalCovidCache.kt @@ -1,18 +1,17 @@ package com.mobiledevpro.data.repository.userdata -import com.mobiledevpro.data.model.CountryEntity +import com.mobiledevpro.data.model.CountryTotalEntity import com.mobiledevpro.data.model.TotalEntity -import com.mobiledevpro.domain.model.Country import io.reactivex.Completable import io.reactivex.Observable -interface CovidCache { +interface TotalCovidCache { fun getTotalDataObservable(): Observable fun updateTotalData(totalEntity: TotalEntity): Completable - fun getLocalCountriesObservable(query: String): Observable> + fun getLocalCountriesObservable(query: String): Observable> - fun updateCountries(countriesEntity: List): Completable + fun updateCountries(countriesTotalEntity: List): Completable } \ No newline at end of file diff --git a/data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidRemote.kt b/data/src/main/java/com/mobiledevpro/data/repository/userdata/TotalCovidRemote.kt similarity index 67% rename from data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidRemote.kt rename to data/src/main/java/com/mobiledevpro/data/repository/userdata/TotalCovidRemote.kt index 6171bc1..87e553b 100644 --- a/data/src/main/java/com/mobiledevpro/data/repository/userdata/CovidRemote.kt +++ b/data/src/main/java/com/mobiledevpro/data/repository/userdata/TotalCovidRemote.kt @@ -1,10 +1,10 @@ package com.mobiledevpro.data.repository.userdata -import com.mobiledevpro.data.model.CountryEntity +import com.mobiledevpro.data.model.CountryTotalEntity import com.mobiledevpro.data.model.TotalValueEntity import io.reactivex.Single -interface CovidRemote { +interface TotalCovidRemote { fun getTotalConfirmed(): Single @@ -12,5 +12,5 @@ interface CovidRemote { fun getTotalRecovered(): Single - fun getCountries(): Single> + fun getCountries(): Single> } \ No newline at end of file diff --git a/domain/src/main/java/com/mobiledevpro/domain/common/Result.kt b/domain/src/main/java/com/mobiledevpro/domain/common/Result.kt index e72b4dd..ce29aeb 100644 --- a/domain/src/main/java/com/mobiledevpro/domain/common/Result.kt +++ b/domain/src/main/java/com/mobiledevpro/domain/common/Result.kt @@ -13,6 +13,7 @@ enum class Error { NOT_FOUND_ERROR, ACCESS_DENIED_ERROR, SERVICE_UNAVAILABLE_ERROR, + HTML_PARSER_ERROR, UNKNOWN_ERROR } diff --git a/domain/src/main/java/com/mobiledevpro/domain/error/Error.kt b/domain/src/main/java/com/mobiledevpro/domain/error/Error.kt index a26098e..e0b8f05 100644 --- a/domain/src/main/java/com/mobiledevpro/domain/error/Error.kt +++ b/domain/src/main/java/com/mobiledevpro/domain/error/Error.kt @@ -8,4 +8,6 @@ class AccessDeniedThrowable : Throwable() class ServiceUnavailableThrowable : Throwable() +class HtmlParserThrowable(override val message: String?) : Throwable() + class UnknownThrowable : Throwable() diff --git a/domain/src/main/java/com/mobiledevpro/domain/extension/MaperExtension.kt b/domain/src/main/java/com/mobiledevpro/domain/extension/MaperExtension.kt index 192843c..564c1d8 100644 --- a/domain/src/main/java/com/mobiledevpro/domain/extension/MaperExtension.kt +++ b/domain/src/main/java/com/mobiledevpro/domain/extension/MaperExtension.kt @@ -4,6 +4,7 @@ import com.mobiledevpro.domain.common.Error import com.mobiledevpro.domain.common.None import com.mobiledevpro.domain.common.Result import com.mobiledevpro.domain.error.AccessDeniedThrowable +import com.mobiledevpro.domain.error.HtmlParserThrowable import com.mobiledevpro.domain.error.NetworkThrowable import com.mobiledevpro.domain.error.NotFoundThrowable import com.mobiledevpro.domain.error.ServiceUnavailableThrowable @@ -46,6 +47,7 @@ private fun Throwable.toView() = when (this) { is NotFoundThrowable -> Error.NOT_FOUND_ERROR is AccessDeniedThrowable -> Error.ACCESS_DENIED_ERROR is ServiceUnavailableThrowable -> Error.SERVICE_UNAVAILABLE_ERROR + is HtmlParserThrowable -> Error.HTML_PARSER_ERROR is UnknownThrowable -> Error.UNKNOWN_ERROR else -> Error.UNKNOWN_ERROR diff --git a/domain/src/main/java/com/mobiledevpro/domain/model/StatisticCountry.kt b/domain/src/main/java/com/mobiledevpro/domain/model/StatisticCountry.kt new file mode 100644 index 0000000..969f6a5 --- /dev/null +++ b/domain/src/main/java/com/mobiledevpro/domain/model/StatisticCountry.kt @@ -0,0 +1,36 @@ +package com.mobiledevpro.domain.model + +/** + * Data class for collect country statistic for view + * @property country is a name of country + * @property province is a name of province by country or by country + * @property longitude is a longitude of coordinates by province + * @property latitude is a latitude of coordinates by province + * @property dayStatistics is a list of statistic for a day + */ +data class StatisticCountry( + val country: String, + val province: String, + val latitude: Double, + val longitude: Double, + val dayStatistics: List +) + +/** + * Data class for collect statistic by day + * @property date is a date of date in format 22/02/20 + * @property totalConfirmed is a confirmed count of people from start pandemic day to current day + * @property totalDeaths is a deaths count of people from start pandemic day to current day + * @property totalRecovered is a recovered count of people from start pandemic day to current day + * @property confirmed is a confirmed count people by day + * @property deaths is a deaths of count people by day + * @property recovered is a recovered count people by day*/ +data class DayStatistic( + val date: Long, + val totalConfirmed: Long, + var confirmed: Long = 0L, + val totalDeaths: Long, + var deaths: Long = 0L, + val totalRecovered: Long, + var recovered: Long = 0L +) \ No newline at end of file diff --git a/domain/src/main/java/com/mobiledevpro/domain/model/Country.kt b/domain/src/main/java/com/mobiledevpro/domain/model/TotalCountry.kt similarity index 94% rename from domain/src/main/java/com/mobiledevpro/domain/model/Country.kt rename to domain/src/main/java/com/mobiledevpro/domain/model/TotalCountry.kt index 91a8dbd..34b7649 100644 --- a/domain/src/main/java/com/mobiledevpro/domain/model/Country.kt +++ b/domain/src/main/java/com/mobiledevpro/domain/model/TotalCountry.kt @@ -8,7 +8,7 @@ package com.mobiledevpro.domain.model * http://androiddev.pro * */ -data class Country( +data class TotalCountry( val id: Int = 0, val country: String = "", val updated: Long = 0, diff --git a/domain/src/main/java/com/mobiledevpro/domain/statistic/data/DefaultStatisticDataInteractor.kt b/domain/src/main/java/com/mobiledevpro/domain/statistic/data/DefaultStatisticDataInteractor.kt new file mode 100644 index 0000000..14b11cf --- /dev/null +++ b/domain/src/main/java/com/mobiledevpro/domain/statistic/data/DefaultStatisticDataInteractor.kt @@ -0,0 +1,48 @@ +package com.mobiledevpro.domain.statistic.data + +import com.mobiledevpro.domain.common.Result +import com.mobiledevpro.domain.extension.toResult +import com.mobiledevpro.domain.model.StatisticCountry +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class DefaultStatisticDataInteractor( + private val statisticDataRepository: StatisticDataRepository +) : StatisticDataInteractor { + + override fun getCountriesStatistics(page: Int) = statisticDataRepository + .getStatisticFromApiByPage(page) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + + override fun fetchStatisticsFromHtml() = statisticDataRepository + .fetchStatisticsFromHtml() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + + override fun observeStatisticByCountryName(query: String): Observable> = + statisticDataRepository + .observeStatisticByCountyName(query) + .map { result -> + result.dayStatistics.mapIndexed { index, dayStatistic -> + if (index == 0) { + dayStatistic.confirmed = result.dayStatistics[index].totalConfirmed + dayStatistic.deaths = result.dayStatistics[index].totalDeaths + dayStatistic.recovered = result.dayStatistics[index].totalRecovered + } else { + dayStatistic.confirmed = + dayStatistic.totalConfirmed - result.dayStatistics[index - 1].totalConfirmed + dayStatistic.deaths = + dayStatistic.totalDeaths - result.dayStatistics[index - 1].totalDeaths + dayStatistic.recovered = + dayStatistic.totalRecovered - result.dayStatistics[index - 1].totalRecovered + } + } + + result + } + .toResult() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) +} \ No newline at end of file diff --git a/domain/src/main/java/com/mobiledevpro/domain/statistic/data/StatisticDataInteractor.kt b/domain/src/main/java/com/mobiledevpro/domain/statistic/data/StatisticDataInteractor.kt new file mode 100644 index 0000000..b087e78 --- /dev/null +++ b/domain/src/main/java/com/mobiledevpro/domain/statistic/data/StatisticDataInteractor.kt @@ -0,0 +1,17 @@ +package com.mobiledevpro.domain.statistic.data + +import com.mobiledevpro.domain.common.Result +import com.mobiledevpro.domain.model.StatisticCountry +import com.mobiledevpro.domain.model.TotalCountry +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single + +interface StatisticDataInteractor { + + fun getCountriesStatistics(page: Int): Single> + + fun fetchStatisticsFromHtml(): Completable + + fun observeStatisticByCountryName(query: String): Observable> +} \ No newline at end of file diff --git a/domain/src/main/java/com/mobiledevpro/domain/statistic/data/StatisticDataRepository.kt b/domain/src/main/java/com/mobiledevpro/domain/statistic/data/StatisticDataRepository.kt new file mode 100644 index 0000000..c47e9d3 --- /dev/null +++ b/domain/src/main/java/com/mobiledevpro/domain/statistic/data/StatisticDataRepository.kt @@ -0,0 +1,16 @@ +package com.mobiledevpro.domain.statistic.data + +import com.mobiledevpro.domain.model.StatisticCountry +import com.mobiledevpro.domain.model.TotalCountry +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single + +interface StatisticDataRepository { + + fun getStatisticFromApiByPage(page: Int): Single> + + fun fetchStatisticsFromHtml(): Completable + + fun observeStatisticByCountyName(query: String): Observable +} \ No newline at end of file diff --git a/domain/src/main/java/com/mobiledevpro/domain/totaldata/DefaultTotalDataInteractor.kt b/domain/src/main/java/com/mobiledevpro/domain/totaldata/DefaultTotalDataInteractor.kt index b358f36..e54f559 100644 --- a/domain/src/main/java/com/mobiledevpro/domain/totaldata/DefaultTotalDataInteractor.kt +++ b/domain/src/main/java/com/mobiledevpro/domain/totaldata/DefaultTotalDataInteractor.kt @@ -3,8 +3,8 @@ package com.mobiledevpro.domain.totaldata import com.mobiledevpro.domain.common.None import com.mobiledevpro.domain.common.Result import com.mobiledevpro.domain.extension.toResult -import com.mobiledevpro.domain.model.Country import com.mobiledevpro.domain.model.Total +import com.mobiledevpro.domain.model.TotalCountry import io.reactivex.Observable import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers @@ -39,7 +39,7 @@ class DefaultTotalDataInteractor( override fun observeCountriesListData( query: String - ): Observable>> = totalDataRepository + ): Observable>> = totalDataRepository .getLocalCountriesObservable(query) .map { ArrayList(it) } .toResult() diff --git a/domain/src/main/java/com/mobiledevpro/domain/totaldata/TotalDataInteractor.kt b/domain/src/main/java/com/mobiledevpro/domain/totaldata/TotalDataInteractor.kt index c76cbdd..f2408db 100644 --- a/domain/src/main/java/com/mobiledevpro/domain/totaldata/TotalDataInteractor.kt +++ b/domain/src/main/java/com/mobiledevpro/domain/totaldata/TotalDataInteractor.kt @@ -2,8 +2,8 @@ package com.mobiledevpro.domain.totaldata import com.mobiledevpro.domain.common.None import com.mobiledevpro.domain.common.Result -import com.mobiledevpro.domain.model.Country import com.mobiledevpro.domain.model.Total +import com.mobiledevpro.domain.model.TotalCountry import io.reactivex.Observable import io.reactivex.Single @@ -22,7 +22,7 @@ interface TotalDataInteractor { fun refreshTotalData(): Single> - fun observeCountriesListData(query: String): Observable>> + fun observeCountriesListData(query: String): Observable>> fun refreshCountriesData(): Single> } diff --git a/domain/src/main/java/com/mobiledevpro/domain/totaldata/TotalDataRepository.kt b/domain/src/main/java/com/mobiledevpro/domain/totaldata/TotalDataRepository.kt index cc9b3fb..2094223 100644 --- a/domain/src/main/java/com/mobiledevpro/domain/totaldata/TotalDataRepository.kt +++ b/domain/src/main/java/com/mobiledevpro/domain/totaldata/TotalDataRepository.kt @@ -1,7 +1,7 @@ package com.mobiledevpro.domain.totaldata -import com.mobiledevpro.domain.model.Country import com.mobiledevpro.domain.model.Total +import com.mobiledevpro.domain.model.TotalCountry import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single @@ -27,9 +27,9 @@ interface TotalDataRepository { fun getTotalData(): Single - fun getLocalCountriesObservable(query: String): Observable> + fun getLocalCountriesObservable(query: String): Observable> - fun getCountries(): Single> + fun getCountries(): Single> - fun setLocalCountriesData(countries: List): Completable + fun setLocalCountriesData(totalCountries: List): Completable }