├── .idea
├── .name
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── misc.xml
├── runConfigurations.xml
└── gradle.xml
├── data
├── consumer-rules.pro
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── data
│ │ │ ├── network
│ │ │ ├── Countries.kt
│ │ │ ├── CountryItem.kt
│ │ │ ├── OverviewItem.kt
│ │ │ ├── DailySummarySub.kt
│ │ │ ├── Overview.kt
│ │ │ ├── DailySummary.kt
│ │ │ ├── Detail.kt
│ │ │ ├── Daily.kt
│ │ │ └── Confirmed.kt
│ │ │ └── local
│ │ │ ├── entity
│ │ │ └── Country.kt
│ │ │ ├── dao
│ │ │ └── CountryDAO.kt
│ │ │ └── CoronaDB.kt
│ ├── test
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── data
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── tech
│ │ └── awesome
│ │ └── data
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── app
├── .gitignore
├── src
│ ├── debug
│ │ └── res
│ │ │ └── values
│ │ │ ├── strings.xml
│ │ │ └── google_maps_api.xml
│ ├── main
│ │ ├── assets
│ │ │ └── fonts
│ │ │ │ ├── Menlo-Bold.ttf
│ │ │ │ └── Menlo-Regular.ttf
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── ic_bs.png
│ │ │ │ ├── ic_info.png
│ │ │ │ ├── ic_map.png
│ │ │ │ ├── ic_select.png
│ │ │ │ ├── ic_symptoms.png
│ │ │ │ ├── splash_logo.png
│ │ │ │ ├── ic_hand_clean.png
│ │ │ │ ├── logo_dashboard.png
│ │ │ │ ├── splash_logo_light.png
│ │ │ │ ├── ic_placeholder_flag.png
│ │ │ │ └── logo_dashboard_light.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── ic_bs.png
│ │ │ │ ├── ic_info.png
│ │ │ │ ├── ic_map.png
│ │ │ │ ├── ic_select.png
│ │ │ │ ├── ic_symptoms.png
│ │ │ │ ├── splash_logo.png
│ │ │ │ ├── ic_hand_clean.png
│ │ │ │ ├── logo_dashboard.png
│ │ │ │ ├── splash_logo_light.png
│ │ │ │ ├── ic_placeholder_flag.png
│ │ │ │ └── logo_dashboard_light.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── ic_bs.png
│ │ │ │ ├── ic_map.png
│ │ │ │ ├── ic_info.png
│ │ │ │ ├── ic_select.png
│ │ │ │ ├── ic_symptoms.png
│ │ │ │ ├── splash_logo.png
│ │ │ │ ├── ic_hand_clean.png
│ │ │ │ ├── logo_dashboard.png
│ │ │ │ ├── ic_placeholder_flag.png
│ │ │ │ ├── splash_logo_light.png
│ │ │ │ └── logo_dashboard_light.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── ic_bs.png
│ │ │ │ ├── ic_info.png
│ │ │ │ ├── ic_map.png
│ │ │ │ ├── ic_select.png
│ │ │ │ ├── ic_hand_clean.png
│ │ │ │ ├── ic_symptoms.png
│ │ │ │ ├── splash_logo.png
│ │ │ │ ├── logo_dashboard.png
│ │ │ │ ├── splash_logo_light.png
│ │ │ │ ├── ic_placeholder_flag.png
│ │ │ │ └── logo_dashboard_light.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ ├── ic_bs.png
│ │ │ │ ├── ic_info.png
│ │ │ │ ├── ic_map.png
│ │ │ │ ├── ic_select.png
│ │ │ │ ├── ic_symptoms.png
│ │ │ │ ├── splash_logo.png
│ │ │ │ ├── ic_hand_clean.png
│ │ │ │ ├── logo_dashboard.png
│ │ │ │ ├── splash_logo_light.png
│ │ │ │ ├── ic_placeholder_flag.png
│ │ │ │ └── logo_dashboard_light.png
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── drawable
│ │ │ │ ├── img_deaths_marker.png
│ │ │ │ ├── img_confirmed_marker.png
│ │ │ │ ├── img_recovered_marker.png
│ │ │ │ ├── bg_indicator_death.xml
│ │ │ │ ├── bg_btn.xml
│ │ │ │ ├── bg_indicator_confirmed.xml
│ │ │ │ ├── bg_indicator_recovered.xml
│ │ │ │ ├── bg_item_menu_content.xml
│ │ │ │ ├── bg_rounded_placeholder.xml
│ │ │ │ ├── splash_view.xml
│ │ │ │ ├── bg_search.xml
│ │ │ │ ├── ic_back_arrow.xml
│ │ │ │ ├── bg_daily_menu_active.xml
│ │ │ │ ├── bg_daily_menu_inactive.xml
│ │ │ │ ├── ic_pie_chart_active.xml
│ │ │ │ ├── ic_pie_chart_inactive.xml
│ │ │ │ ├── ic_color_mode.xml
│ │ │ │ ├── ic_search.xml
│ │ │ │ ├── ic_graph_active.xml
│ │ │ │ ├── ic_graph_inactive.xml
│ │ │ │ ├── ic_flag.xml
│ │ │ │ ├── ic_edit.xml
│ │ │ │ ├── ic_map_2.xml
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── margins.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable-night
│ │ │ │ ├── splash_view.xml
│ │ │ │ ├── ic_back_arrow.xml
│ │ │ │ └── bg_item_menu_content.xml
│ │ │ ├── drawable-notnight-v29
│ │ │ │ └── splash_view.xml
│ │ │ ├── layout
│ │ │ │ ├── item_daily_loading.xml
│ │ │ │ ├── fragment_maps.xml
│ │ │ │ ├── activity_webview.xml
│ │ │ │ ├── item_region.xml
│ │ │ │ ├── item_region_selected.xml
│ │ │ │ ├── item_daily_content.xml
│ │ │ │ ├── item_maps_content.xml
│ │ │ │ └── activity_maps.xml
│ │ │ ├── values-night
│ │ │ │ ├── styles.xml
│ │ │ │ └── colors.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── raw
│ │ │ │ └── style_json.json
│ │ ├── java
│ │ │ └── tech
│ │ │ │ └── awesome
│ │ │ │ └── coronatrack
│ │ │ │ ├── ui
│ │ │ │ ├── base
│ │ │ │ │ ├── BindingViewModel.kt
│ │ │ │ │ ├── BindingActivity.kt
│ │ │ │ │ ├── BindingFragment.kt
│ │ │ │ │ └── BaseActivity.kt
│ │ │ │ ├── splashscreen
│ │ │ │ │ └── SplashActivity.kt
│ │ │ │ ├── maps
│ │ │ │ │ ├── MapsViewModel.kt
│ │ │ │ │ ├── fragment
│ │ │ │ │ │ └── adapter
│ │ │ │ │ │ │ └── MapsAdapter.kt
│ │ │ │ │ └── MapsActivity.kt
│ │ │ │ ├── daily_updates
│ │ │ │ │ ├── DailyUpdatesViewModel.kt
│ │ │ │ │ └── adapter
│ │ │ │ │ │ └── DailyUpdatesAdapter.kt
│ │ │ │ ├── add_country
│ │ │ │ │ ├── AddCountryViewModel.kt
│ │ │ │ │ └── adapter
│ │ │ │ │ │ └── AddCountryAdapter.kt
│ │ │ │ ├── webview
│ │ │ │ │ └── WebviewActivity.kt
│ │ │ │ └── main
│ │ │ │ │ └── MainViewModel.kt
│ │ │ │ ├── di
│ │ │ │ ├── RepositoryModule.kt
│ │ │ │ ├── ViewModelModule.kt
│ │ │ │ ├── PersistenceModule.kt
│ │ │ │ ├── AppModule.kt
│ │ │ │ └── NetworkModule.kt
│ │ │ │ ├── binding
│ │ │ │ └── ViewBinding.kt
│ │ │ │ ├── widget
│ │ │ │ └── StatusText.kt
│ │ │ │ └── Application.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── coronatrack
│ │ │ ├── InstantTaskExecution.kt
│ │ │ ├── CoroutineRule.kt
│ │ │ └── ui
│ │ │ └── maps
│ │ │ └── MapsViewModelTest.kt
│ ├── androidTest
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── coronatrack
│ │ │ └── ExampleInstrumentedTest.kt
│ └── release
│ │ └── res
│ │ └── values
│ │ └── google_maps_api.xml
├── proguard-rules.pro
└── build.gradle
├── domain
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── domain
│ │ │ ├── pref
│ │ │ ├── AppPref.kt
│ │ │ └── IAppPref.kt
│ │ │ ├── local
│ │ │ ├── DBRepository.kt
│ │ │ └── IDBRepository.kt
│ │ │ └── network
│ │ │ ├── Repository.kt
│ │ │ └── IRepository.kt
│ ├── test
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── domain
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── tech
│ │ └── awesome
│ │ └── domain
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── network
├── consumer-rules.pro
├── .gitignore
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── network
│ │ │ ├── CacheProvider.kt
│ │ │ ├── Api.kt
│ │ │ └── ICacheProvider.kt
│ ├── test
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── network
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── tech
│ │ └── awesome
│ │ └── network
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── utils
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── drawable-hdpi
│ │ │ │ └── ic_placeholder_flag.png
│ │ │ ├── drawable-mdpi
│ │ │ │ └── ic_placeholder_flag.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ └── ic_placeholder_flag.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ └── ic_placeholder_flag.png
│ │ │ └── drawable-xxxhdpi
│ │ │ │ └── ic_placeholder_flag.png
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── utils
│ │ │ ├── extension
│ │ │ ├── LongExt.kt
│ │ │ ├── IntExt.kt
│ │ │ ├── ThrowableExt.kt
│ │ │ ├── LifecycleExt.kt
│ │ │ ├── ContextExt.kt
│ │ │ ├── ViewExt.kt
│ │ │ ├── ImageExt.kt
│ │ │ ├── AppCompatActivityExt.kt
│ │ │ └── StringExt.kt
│ │ │ ├── Animator.kt
│ │ │ ├── Either.kt
│ │ │ ├── State.kt
│ │ │ ├── Constant.kt
│ │ │ └── Saver.kt
│ ├── test
│ │ └── java
│ │ │ └── tech
│ │ │ └── awesome
│ │ │ └── utils
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── tech
│ │ └── awesome
│ │ └── utils
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── .gitignore
├── README.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | Corona Track
--------------------------------------------------------------------------------
/data/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/domain/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/network/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/utils/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/utils/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/network/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/data/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Coronadb
3 |
4 |
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Corona Track (Dev)
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/assets/fonts/Menlo-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/assets/fonts/Menlo-Bold.ttf
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_bs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/ic_bs.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_bs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/ic_bs.png
--------------------------------------------------------------------------------
/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/ic_map.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/ic_map.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_bs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/ic_bs.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/ic_map.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_bs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/ic_bs.png
--------------------------------------------------------------------------------
/domain/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/network/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/app/src/main/assets/fonts/Menlo-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/assets/fonts/Menlo-Regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/ic_select.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/ic_select.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/ic_select.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/ic_map.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_bs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/ic_bs.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/ic_map.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name='Corona Track'
2 | include ':app'
3 | include ':domain'
4 | include ':network'
5 | include ':utils'
6 | include ':data'
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_symptoms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/ic_symptoms.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/splash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/splash_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_symptoms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/ic_symptoms.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/splash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/splash_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_symptoms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/ic_symptoms.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/splash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/splash_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/ic_select.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/ic_select.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img_deaths_marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable/img_deaths_marker.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_hand_clean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/ic_hand_clean.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/logo_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/logo_dashboard.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_hand_clean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/ic_hand_clean.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/logo_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/logo_dashboard.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_hand_clean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/ic_hand_clean.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/logo_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/logo_dashboard.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_hand_clean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/ic_hand_clean.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_symptoms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/ic_symptoms.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/splash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/splash_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_symptoms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/ic_symptoms.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/splash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/splash_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img_confirmed_marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable/img_confirmed_marker.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img_recovered_marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable/img_recovered_marker.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/splash_logo_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/splash_logo_light.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/splash_logo_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/splash_logo_light.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/logo_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/logo_dashboard.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_hand_clean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/ic_hand_clean.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/logo_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/logo_dashboard.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/logo_dashboard_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-hdpi/logo_dashboard_light.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/logo_dashboard_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-mdpi/logo_dashboard_light.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/splash_logo_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/splash_logo_light.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/splash_logo_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/splash_logo_light.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/splash_logo_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/splash_logo_light.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/logo_dashboard_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xhdpi/logo_dashboard_light.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/logo_dashboard_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxhdpi/logo_dashboard_light.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #222934
4 |
--------------------------------------------------------------------------------
/utils/src/main/res/drawable-hdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/utils/src/main/res/drawable-hdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/utils/src/main/res/drawable-mdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/utils/src/main/res/drawable-mdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/utils/src/main/res/drawable-xhdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/utils/src/main/res/drawable-xhdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/logo_dashboard_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/app/src/main/res/drawable-xxxhdpi/logo_dashboard_light.png
--------------------------------------------------------------------------------
/utils/src/main/res/drawable-xxhdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/utils/src/main/res/drawable-xxhdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/utils/src/main/res/drawable-xxxhdpi/ic_placeholder_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agungnursatria/corona_tracker_19/HEAD/utils/src/main/res/drawable-xxxhdpi/ic_placeholder_flag.png
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/base/BindingViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.base
2 |
3 | import androidx.lifecycle.ViewModel
4 |
5 | abstract class BindingViewModel : ViewModel()
--------------------------------------------------------------------------------
/utils/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/Countries.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class Countries (
7 | val countries: List?
8 | )
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_indicator_death.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 29 13:25:19 WIB 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_indicator_confirmed.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_indicator_recovered.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_item_menu_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_rounded_placeholder.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/CountryItem.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class CountryItem (
7 | val name: String,
8 | val iso2: String?,
9 | val iso3: String?
10 | )
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.di
2 |
3 | import org.koin.dsl.module
4 | import tech.awesome.domain.network.Repository
5 | import tech.awesome.domain.network.IRepository
6 |
7 | val repositoryModule = module {
8 | single { IRepository(get()) as Repository }
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/splash_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-notnight-v29/splash_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_back_arrow.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/ic_back_arrow.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/OverviewItem.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import android.os.Parcelable
4 | import com.squareup.moshi.JsonClass
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @JsonClass(generateAdapter = true)
8 | @Parcelize
9 | data class OverviewItem(
10 | val detail: String = "",
11 | val value: Int = 0
12 | ) : Parcelable
--------------------------------------------------------------------------------
/domain/src/main/java/tech/awesome/domain/pref/AppPref.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.domain.pref
2 |
3 | interface AppPref {
4 | fun setCountryPrimaryKey(countryName: String)
5 | fun getCountryName() : String?
6 | fun setColorMode(isNightMode: Boolean)
7 | fun getColorMode() : Boolean
8 | fun setFirstTime(isFirstTime: Boolean)
9 | fun getFirstTime() : Boolean
10 | }
--------------------------------------------------------------------------------
/network/src/main/java/tech/awesome/network/CacheProvider.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.network
2 |
3 | import okhttp3.Cache
4 | import okhttp3.CacheControl
5 | import okhttp3.Interceptor
6 | import okhttp3.Response
7 | import java.io.File
8 | import java.util.concurrent.TimeUnit
9 |
10 | interface CacheProvider {
11 | fun getInterceptor(): Interceptor
12 | val cache: Cache
13 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/bg_item_menu_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_daily_menu_active.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/LongExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import tech.awesome.utils.FormatConstant
4 | import java.text.SimpleDateFormat
5 | import java.util.*
6 |
7 | fun Long.formattedTime(): String {
8 | val sdf = SimpleDateFormat(FormatConstant.FORMAT_DATE_LATEST_UPDATE, Locale.getDefault())
9 | return sdf.format(Date(this))
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_daily_menu_inactive.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/DailySummarySub.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import android.os.Parcelable
4 | import com.squareup.moshi.JsonClass
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @JsonClass(generateAdapter = true)
8 | @Parcelize
9 | data class DailySummarySub(
10 | val total: Int?,
11 | val china: Int?,
12 | val outsideChina: Int?
13 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/res/values/margins.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 32dp
4 | 24dp
5 | 16dp
6 | 12dp
7 | 8dp
8 | 4dp
9 |
10 | 550dp
11 | 250dp
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pie_chart_active.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pie_chart_inactive.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/data/src/test/java/tech/awesome/data/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/IntExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import android.content.res.Resources
4 | import java.text.NumberFormat
5 | import java.util.*
6 |
7 | fun Int.separatedNumber(): String = NumberFormat.getNumberInstance(Locale.getDefault()).format(this)
8 | fun Int.toDp(): Int = (this / Resources.getSystem().displayMetrics.density).toInt()
9 | fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).toInt()
--------------------------------------------------------------------------------
/utils/src/test/java/tech/awesome/utils/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/domain/src/test/java/tech/awesome/domain/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.domain
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/network/src/test/java/tech/awesome/network/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.network
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/local/entity/Country.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.local.entity
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "country")
7 | data class Country(
8 | @PrimaryKey
9 | var name: String = "",
10 | var countryName: String = "",
11 | var flag: String? = null,
12 | var latitude: Double? = null,
13 | var longitude: Double? = null,
14 | var provinceName: String? = null
15 | )
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/base/BindingActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.base
2 |
3 | import androidx.annotation.LayoutRes
4 | import androidx.databinding.DataBindingUtil
5 | import androidx.databinding.ViewDataBinding
6 |
7 | abstract class BindingActivity : BaseActivity() {
8 |
9 | protected inline fun binding(
10 | @LayoutRes resId: Int
11 | ): Lazy = lazy { DataBindingUtil.setContentView(this, resId) }
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_color_mode.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_daily_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/ThrowableExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import retrofit2.HttpException
4 | import tech.awesome.utils.NetworkConstant
5 | import java.io.IOException
6 | import java.net.SocketTimeoutException
7 |
8 | fun Throwable.getError(): String =
9 | when (this) {
10 | is HttpException -> NetworkConstant.OFFLINE_MESSAGE
11 | is SocketTimeoutException -> NetworkConstant.ERROR_MESSAGE
12 | is IOException -> NetworkConstant.ERROR_MESSAGE
13 | else -> localizedMessage ?: NetworkConstant.ERROR_MESSAGE
14 | }
15 |
--------------------------------------------------------------------------------
/domain/src/main/java/tech/awesome/domain/local/DBRepository.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.domain.local
2 |
3 | import tech.awesome.data.local.entity.Country
4 | import tech.awesome.utils.Either
5 |
6 | interface DBRepository {
7 | suspend fun getCountry() : Either>
8 | suspend fun getCountry(name: String) : Either>
9 | suspend fun insertCountry(country: Country) : Either
10 | suspend fun updateCountry(country: Country) : Either
11 | suspend fun deleteCountry(name: String) : Either
12 | }
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/LifecycleExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.Observer
7 |
8 | fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) {
9 | liveData.observe(this, Observer { it?.let { t -> action(t) } })
10 | }
11 |
12 | fun LifecycleOwner.observe(liveData: MutableLiveData, action: (t: T) -> Unit) {
13 | liveData.observe(this, Observer { it?.let { t -> action(t) } })
14 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/Overview.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import android.os.Parcelable
4 | import com.squareup.moshi.JsonClass
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @JsonClass(generateAdapter = true)
8 | @Parcelize
9 | data class Overview(
10 | val confirmed: OverviewItem = OverviewItem(
11 | "",
12 | 0
13 | ),
14 | val recovered: OverviewItem = OverviewItem(
15 | "",
16 | 0
17 | ),
18 | val deaths: OverviewItem = OverviewItem(
19 | "",
20 | 0
21 | ),
22 | val lastUpdate: String? = null
23 | ) : Parcelable
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import android.content.Context
4 | import android.content.res.Configuration
5 | import android.os.Build
6 | import androidx.annotation.ColorRes
7 |
8 | fun Context.getColorFixed(@ColorRes color: Int): Int {
9 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
10 | getColor(color)
11 | } else {
12 | resources.getColor(color)
13 | }
14 | }
15 |
16 | fun Context.isDarkMode() : Boolean {
17 | return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/base/BindingFragment.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.base
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.annotation.LayoutRes
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.databinding.ViewDataBinding
8 | import androidx.fragment.app.Fragment
9 |
10 | abstract class BindingFragment : Fragment() {
11 |
12 | protected inline fun binding(
13 | inflater: LayoutInflater,
14 | @LayoutRes resId: Int,
15 | container: ViewGroup?
16 | ): T = DataBindingUtil.inflate(inflater, resId, container, false)
17 | }
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/DailySummary.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import android.os.Parcelable
4 | import com.squareup.moshi.JsonClass
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @JsonClass(generateAdapter = true)
8 | @Parcelize
9 | data class DailySummary(
10 | val totalConfirmed: Int?,
11 | val mainlandChina: Int?,
12 | val otherLocations: Int?,
13 | val deltaConfirmed: Int?,
14 | val totalRecovered: Int?,
15 | val deltaRecovered: Int?,
16 | val confirmed: DailySummarySub?,
17 | val deaths: DailySummarySub?,
18 | val recovered: DailySummarySub?,
19 | val reportDate: String?
20 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/di/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.di
2 |
3 | import org.koin.android.viewmodel.dsl.viewModel
4 | import org.koin.dsl.module
5 | import tech.awesome.coronatrack.ui.add_country.AddCountryViewModel
6 | import tech.awesome.coronatrack.ui.daily_updates.DailyUpdatesViewModel
7 | import tech.awesome.coronatrack.ui.main.MainViewModel
8 | import tech.awesome.coronatrack.ui.maps.MapsViewModel
9 |
10 | val viewModelModule = module {
11 | viewModel { MainViewModel(get(), get()) }
12 | viewModel { DailyUpdatesViewModel(get(), get()) }
13 | viewModel { AddCountryViewModel(get(), get()) }
14 | viewModel { MapsViewModel(get(), get()) }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/di/PersistenceModule.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.di
2 |
3 | import org.koin.android.ext.koin.androidContext
4 | import org.koin.dsl.module
5 | import tech.awesome.data.local.CoronaDB
6 | import tech.awesome.domain.local.DBRepository
7 | import tech.awesome.domain.local.IDBRepository
8 | import tech.awesome.domain.pref.AppPref
9 | import tech.awesome.domain.pref.IAppPref
10 |
11 | val persistenceModule = module {
12 | single {
13 | CoronaDB.getDB(androidContext())
14 | }
15 |
16 | single {
17 | IAppPref() as AppPref
18 | }
19 |
20 | single {
21 | IDBRepository(get()) as DBRepository
22 | }
23 |
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_graph_active.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_graph_inactive.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/Detail.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import android.os.Parcelable
4 | import com.squareup.moshi.JsonClass
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @JsonClass(generateAdapter = true)
8 | @Parcelize
9 | data class Detail(
10 | val confirmed: Int?,
11 | val countryRegion: String?,
12 | val deaths: Int?,
13 | val lastUpdate: Long?,
14 | val lat: Double?,
15 | val long: Double?,
16 | val provinceState: String? = null,
17 | val recovered: Int?
18 | ) : Parcelable {
19 | val locationName get() = countryRegion + if (!provinceState.isNullOrEmpty()) ", $provinceState" else ""
20 | val compositeKey get() = countryRegion + provinceState
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_maps.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/Animator.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils
2 |
3 | import android.animation.ValueAnimator
4 | import android.widget.TextView
5 | import java.text.NumberFormat
6 |
7 | object Animator {
8 | fun animateIncrementNumber(
9 | view: TextView,
10 | finalValue: Int = 0,
11 | initialValue: Int = 0
12 | ) {
13 | val valueAnimator = ValueAnimator.ofInt(initialValue, finalValue)
14 | valueAnimator.duration = 1000L
15 | valueAnimator.addUpdateListener { value ->
16 | view.text = NumberFormat.getIntegerInstance()
17 | .format(value.animatedValue.toString().toIntOrNull() ?: 0)
18 | }
19 | valueAnimator.start()
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_flag.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/Daily.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import android.os.Parcelable
4 | import com.squareup.moshi.JsonClass
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @JsonClass(generateAdapter = true)
8 | @Parcelize
9 | data class Daily(
10 | val provinceState: String?,
11 | val countryRegion: String?,
12 | val lastUpdate: String?,
13 | val lat: String?,
14 | val long: String?,
15 | val confirmed: Int = 0,
16 | val recovered: Int = 0,
17 | val deaths: Int = 0,
18 | val active: Int?,
19 | val iso2: String?,
20 | val iso3: String?
21 | ) : Parcelable {
22 | val location get() = if (provinceState != null) "$provinceState, $countryRegion" else countryRegion
23 | }
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/Either.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils
2 |
3 | sealed class Either {
4 | data class Failure(val error: E) : Either()
5 | data class Success(val value: V) : Either()
6 | }
7 |
8 | fun value(value: V): Either =
9 | Either.Success(value)
10 |
11 | fun failure(value: E): Either =
12 | Either.Failure(value)
13 |
14 | fun Either.value(): V {
15 | return (this as Either.Success).value
16 | }
17 |
18 | fun Either.error(): E {
19 | return (this as Either.Failure).error
20 | }
21 |
22 | fun runService(service: S): Either =
23 | runCatching { value(service) }.getOrElse { error -> failure(error) }
--------------------------------------------------------------------------------
/app/src/test/java/tech/awesome/coronatrack/InstantTaskExecution.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack
2 |
3 | import androidx.arch.core.executor.ArchTaskExecutor
4 | import androidx.arch.core.executor.TaskExecutor
5 |
6 | object InstantRuleExecution {
7 | fun start() {
8 | ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
9 | override fun executeOnDiskIO(runnable: Runnable) {
10 | runnable.run()
11 | }
12 |
13 | override fun isMainThread() = true
14 |
15 | override fun postToMainThread(runnable: Runnable) {
16 | runnable.run()
17 | }
18 | })
19 | }
20 |
21 | fun tearDown() {
22 | ArchTaskExecutor.getInstance().setDelegate(null)
23 | }
24 | }
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/ViewExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import androidx.core.content.ContextCompat
6 | import com.facebook.shimmer.ShimmerFrameLayout
7 |
8 | fun View.visible(){
9 | this.visibility = View.VISIBLE
10 | }
11 |
12 | fun View.gone(){
13 | this.visibility = View.GONE
14 | }
15 |
16 | fun View.invisible(){
17 | this.visibility = View.INVISIBLE
18 | }
19 |
20 | fun ShimmerFrameLayout.startShimmering() {
21 | visible()
22 | startShimmer()
23 | }
24 |
25 | fun ShimmerFrameLayout.stopShimmering() {
26 | invisible()
27 | stopShimmer()
28 | }
29 |
30 | fun Context.color(resource: Int): Int {
31 | return ContextCompat.getColor(this, resource)
32 | }
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/local/dao/CountryDAO.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.local.dao
2 |
3 | import androidx.room.*
4 | import tech.awesome.data.local.entity.Country
5 |
6 | @Dao
7 | interface CountryDAO {
8 |
9 | @Query("select * from country where name like :name")
10 | suspend fun get(name: String): List
11 |
12 | @Query("select * from country")
13 | suspend fun get(): List
14 |
15 | @Query("DELETE FROM country")
16 | suspend fun clear()
17 |
18 | @Insert(onConflict = OnConflictStrategy.REPLACE)
19 | suspend fun insert(vararg country: Country)
20 |
21 | @Query("delete from country where name like :name")
22 | suspend fun delete(name: String)
23 |
24 | @Update
25 | suspend fun update(vararg country: Country)
26 |
27 | }
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/network/Confirmed.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.network
2 |
3 | import android.os.Parcelable
4 | import com.squareup.moshi.JsonClass
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @JsonClass(generateAdapter = true)
8 | @Parcelize
9 | data class Confirmed(
10 | val provinceState: String?,
11 | val countryRegion: String?,
12 | val lastUpdate: Long?,
13 | val lat: Double?,
14 | val long: Double?,
15 | val confirmed: Int?,
16 | val recovered: Int?,
17 | val deaths: Int?,
18 | val active: Int?,
19 | val admin2: String?,
20 | val iso2: String?,
21 | val iso3: String?
22 | ) : Parcelable {
23 | val detailname get() = if (provinceState != null) "$provinceState, $countryRegion" else countryRegion
24 | var flag : String? = null
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.di
2 |
3 | import io.github.inflationx.calligraphy3.CalligraphyConfig
4 | import io.github.inflationx.calligraphy3.CalligraphyInterceptor
5 | import io.github.inflationx.viewpump.ViewPump
6 | import org.koin.dsl.module
7 | import tech.awesome.coronatrack.R
8 |
9 | val appModule = module {
10 |
11 | // single {
12 | // CalligraphyConfig.Builder()
13 | // .setDefaultFontPath("fonts/Menlo-Regular.ttf")
14 | // .setFontAttrId(R.attr.fontPath)
15 | // .build()
16 | // }
17 |
18 | // single {
19 | // CalligraphyInterceptor(get())
20 | // }
21 |
22 | // single {
23 | // ViewPump.builder()
24 | // .addInterceptor(get())
25 | // .build()
26 | // }
27 |
28 | }
--------------------------------------------------------------------------------
/data/src/androidTest/java/tech/awesome/data/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("tech.awesome.data.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_edit.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
21 |
--------------------------------------------------------------------------------
/utils/src/androidTest/java/tech/awesome/utils/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("tech.awesome.utils.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/domain/src/androidTest/java/tech/awesome/domain/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.domain
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("tech.awesome.domain.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/tech/awesome/coronatrack/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("tech.awesome.coronatrack", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/network/src/androidTest/java/tech/awesome/network/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.network
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("tech.awesome.network.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/data/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/domain/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/network/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/utils/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/domain/src/main/java/tech/awesome/domain/pref/IAppPref.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.domain.pref
2 |
3 | import tech.awesome.utils.PrefKey
4 | import tech.awesome.utils.Saver
5 |
6 | class IAppPref : AppPref {
7 | override fun setCountryPrimaryKey(countryName: String) =
8 | Saver.instance().saveString(PrefKey.Country, countryName)
9 |
10 | override fun getCountryName(): String? = Saver.instance().getString(PrefKey.Country)
11 |
12 | override fun setColorMode(isNightMode: Boolean) = Saver.instance().saveBoolean(PrefKey.NightMode, isNightMode)
13 |
14 | override fun getColorMode(): Boolean = Saver.instance().getBoolean(PrefKey.NightMode, false)
15 |
16 | override fun setFirstTime(isFirstTime: Boolean) = Saver.instance().saveBoolean(PrefKey.IsFirstTime, isFirstTime)
17 |
18 | override fun getFirstTime(): Boolean = Saver.instance().getBoolean(PrefKey.IsFirstTime, true)
19 | }
--------------------------------------------------------------------------------
/app/src/release/res/values/google_maps_api.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 | YOUR_KEY_HERE
20 |
21 |
--------------------------------------------------------------------------------
/domain/src/main/java/tech/awesome/domain/network/Repository.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.domain.network
2 |
3 | import tech.awesome.utils.Either
4 | import tech.awesome.data.network.Confirmed
5 | import tech.awesome.data.network.Countries
6 | import tech.awesome.data.network.Daily
7 | import tech.awesome.data.network.DailySummary
8 | import tech.awesome.data.network.Overview
9 |
10 | interface Repository {
11 | suspend fun overview(): Either
12 |
13 | suspend fun overviewCountry(country: String): Either
14 |
15 | suspend fun confirmed(): Either>
16 |
17 | suspend fun confirmedCountry(country: String): Either>
18 |
19 | suspend fun dailySummary(): Either>
20 |
21 | suspend fun daily(date: String): Either>
22 |
23 | suspend fun countries(): Either
24 | }
25 |
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/State.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils
2 |
3 | //import tech.awesome.data.db.Country
4 | //import tech.awesome.data.network.Countries
5 | //import tech.awesome.data.network.CountryItem
6 | //import tech.awesome.data.network.Daily
7 | //import tech.awesome.data.network.DailySummary
8 | //import tech.awesome.data.network.Overview
9 |
10 | sealed class State {
11 | data class Error(val message: E) : State()
12 | data class Success(val value: V) : State()
13 | object Loading : State()
14 | }
15 |
16 | //typealias OverviewState = State
17 | //typealias CountryItemState = State
18 | //typealias CountriesState = State
19 | //typealias CountryState = State
20 | //typealias DailySummaryState = State>
21 | //typealias DailyState = State>
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/ImageExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.widget.ImageView
6 | import androidx.annotation.DrawableRes
7 | import coil.api.load
8 | import coil.transform.CircleCropTransformation
9 | import tech.awesome.utils.R
10 |
11 | fun ImageView.loadImage(url: String) {
12 | load(url) {
13 | crossfade(true)
14 | placeholder(R.drawable.ic_placeholder_flag)
15 | error(R.drawable.ic_placeholder_flag)
16 | // transformations(CircleCropTransformation())
17 | }
18 | }
19 |
20 |
21 | fun ImageView.setAssetImage(context: Context, @DrawableRes drawableRes: Int) {
22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
23 | setImageDrawable(context.getDrawable(drawableRes))
24 | } else {
25 | setImageDrawable(context.resources.getDrawable(drawableRes))
26 | }
27 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Corona Track
2 | Android covid19 data monitoring using MVVM, Coroutine, Room, Data Binding, Kotlin, Night/Dark Mode, Modularization, UnitTest etc
3 |
4 | 
5 |
6 | Library References:
7 | - [Koin](https://github.com/InsertKoinIO/koin)
8 | - [Retrofit](https://github.com/square/retrofit)
9 | - Room
10 | - [MPAndroidChart](https://github.com/PhilJay/MPAndroidChart)
11 | - [Calligraphy](https://github.com/InflationX/Calligraphy)
12 | - [Moshi](https://github.com/square/moshi)
13 | - [Coil](https://github.com/coil-kt/coil)
14 | - [Stetho](https://github.com/facebook/stetho)
15 | - [Coroutine](https://github.com/Kotlin/kotlinx.coroutines)
16 | - [Facebook Shimmering](https://github.com/facebook/Shimmer)
17 | - [Mockito](https://github.com/mockito/mockito)
18 | - etc
19 |
20 | App References:
21 | [kotlin-mvvm-covid19](https://github.com/rizmaulana/kotlin-mvvm-covid19)
22 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/network/src/main/java/tech/awesome/network/Api.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.network
2 |
3 | import retrofit2.http.GET
4 | import retrofit2.http.Path
5 | import tech.awesome.data.network.Confirmed
6 | import tech.awesome.data.network.Countries
7 | import tech.awesome.data.network.Daily
8 | import tech.awesome.data.network.DailySummary
9 | import tech.awesome.data.network.Overview
10 |
11 | interface Api {
12 | @GET("api")
13 | suspend fun overview(): Overview
14 |
15 | @GET("api/countries/{country}")
16 | suspend fun overviewCountry(@Path("country") country: String): Overview
17 |
18 | @GET("api/confirmed")
19 | suspend fun confirmed(): List
20 |
21 | @GET("api/countries/{country}/confirmed")
22 | suspend fun confirmedCountry(@Path("country") country: String): List
23 |
24 | @GET("api/daily")
25 | suspend fun dailySummary(): List
26 |
27 | @GET("api/daily/{date}")
28 | suspend fun daily(@Path("date") date: String): List
29 |
30 | @GET("api/countries")
31 | suspend fun countries(): Countries
32 | }
--------------------------------------------------------------------------------
/network/src/main/java/tech/awesome/network/ICacheProvider.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.network
2 |
3 | import okhttp3.Cache
4 | import okhttp3.CacheControl
5 | import okhttp3.Interceptor
6 | import okhttp3.Response
7 | import java.io.File
8 | import java.util.concurrent.TimeUnit
9 |
10 | class ICacheProvider(private val cacheDir: File): CacheProvider, Interceptor {
11 | private val cacheControl by lazy {
12 | CacheControl.Builder()
13 | .maxStale(1, TimeUnit.HOURS)
14 | .maxAge(1, TimeUnit.HOURS)
15 | .build()
16 | }
17 |
18 | override val cache by lazy {
19 | Cache(cacheDir, 10 * 1024 * 1024)
20 | }
21 |
22 | override fun getInterceptor(): Interceptor {
23 | return this
24 | }
25 |
26 | override fun intercept(chain: Interceptor.Chain): Response {
27 | val response = chain.proceed(chain.request())
28 |
29 | return response.newBuilder()
30 | .removeHeader("Cache-Control")
31 | .header("Cache-Control", cacheControl.toString())
32 | .build();
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/app/src/debug/res/values/google_maps_api.xml:
--------------------------------------------------------------------------------
1 |
2 |
23 | YOUR_KEY_HERE
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/tech/awesome/coronatrack/CoroutineRule.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.test.TestCoroutineDispatcher
6 | import kotlinx.coroutines.test.resetMain
7 | import kotlinx.coroutines.test.runBlockingTest
8 | import kotlinx.coroutines.test.setMain
9 | import org.junit.rules.TestWatcher
10 | import org.junit.runner.Description
11 |
12 | @ExperimentalCoroutinesApi
13 | class CoroutineRule(
14 | val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
15 | ) : TestWatcher() {
16 |
17 | override fun starting(description: Description?) {
18 | super.starting(description)
19 | Dispatchers.setMain(testDispatcher)
20 | }
21 |
22 | override fun finished(description: Description?) {
23 | super.finished(description)
24 | Dispatchers.resetMain()
25 | testDispatcher.cleanupTestCoroutines()
26 | }
27 | }
28 |
29 | @ExperimentalCoroutinesApi
30 | fun CoroutineRule.runBlockingTest(block: suspend () -> Unit) =
31 | this.testDispatcher.runBlockingTest {
32 | block()
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @color/confirmed
4 | @color/bg_canvas
5 | @color/confirmed
6 |
7 | #222A34
8 | #222A34
9 | #283039
10 | #3b4653
11 | @color/bg_canvas
12 | #4b596a
13 | #3b4653
14 | #ffffff
15 | #ffffff
16 |
17 | #00fff5
18 | #ff0000
19 | #20ed67
20 |
21 | #8000fff5
22 | #80ff0000
23 | #8020ed67
24 |
25 | #bbbbbb
26 | #7d838b
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @color/confirmed
4 | @color/bg_canvas
5 | @color/confirmed
6 |
7 | #ffffff
8 | #222A34
9 | #283039
10 | #3b4653
11 | @color/bg_canvas
12 | #dddddd
13 | #3b4653
14 | #222A34
15 | #283039
16 |
17 | #00fff5
18 | #ff0000
19 | #20ed67
20 |
21 | #8000fff5
22 | #80ff0000
23 | #8020ed67
24 |
25 | #bbbbbb
26 | #7d838b
27 |
28 |
--------------------------------------------------------------------------------
/data/src/main/java/tech/awesome/data/local/CoronaDB.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.data.local
2 |
3 | import android.content.Context
4 | import androidx.room.Database
5 | import androidx.room.Room
6 | import androidx.room.RoomDatabase
7 | import tech.awesome.data.R
8 | import tech.awesome.data.local.dao.CountryDAO
9 | import tech.awesome.data.local.entity.Country
10 |
11 |
12 | @Database(entities = [Country::class], version = 5, exportSchema = false)
13 | abstract class CoronaDB : RoomDatabase() {
14 |
15 | companion object {
16 | @Volatile
17 | private var INSTANCE: CoronaDB? = null
18 |
19 | fun getDB(context: Context): CoronaDB {
20 | val temp =
21 | INSTANCE
22 | temp?.let { return temp }
23 |
24 | val instance = Room.databaseBuilder(
25 | context.applicationContext, CoronaDB::class.java,
26 | context.getString(R.string.db_name)
27 | )
28 | .allowMainThreadQueries()
29 | .fallbackToDestructiveMigration()
30 | .build()
31 |
32 | INSTANCE = instance
33 | return instance
34 | }
35 | }
36 |
37 | abstract fun countryDao(): CountryDAO
38 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | kapt.incremental.apt=true
23 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.base
2 |
3 | import android.content.Context
4 | import android.view.MenuItem
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.appcompat.widget.Toolbar
7 | import io.github.inflationx.viewpump.ViewPumpContextWrapper
8 | import tech.awesome.coronatrack.R
9 |
10 | abstract class BaseActivity: AppCompatActivity() {
11 |
12 | protected fun setupToolbar(toolbar: Toolbar, needHomeButton: Boolean = false) {
13 | setSupportActionBar(toolbar)
14 | supportActionBar?.let {
15 | it.setHomeButtonEnabled(true)
16 | it.setDisplayHomeAsUpEnabled(needHomeButton)
17 | it.setDisplayShowTitleEnabled(false)
18 | it.setHomeAsUpIndicator(R.drawable.ic_back_arrow)
19 | }
20 | }
21 |
22 |
23 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
24 | when (item.itemId) {
25 | android.R.id.home -> {
26 | onBackPressed()
27 | return true
28 | }
29 | }
30 |
31 | return super.onOptionsItemSelected(item)
32 | }
33 |
34 | override fun attachBaseContext(newBase: Context) {
35 | super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase))
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
17 |
18 |
21 |
22 |
23 |
24 |
27 |
28 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/splashscreen/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.splashscreen
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.appcompat.app.AppCompatDelegate
6 | import org.koin.android.ext.android.inject
7 | import tech.awesome.coronatrack.R
8 | import tech.awesome.coronatrack.ui.main.MainActivity
9 | import tech.awesome.domain.pref.AppPref
10 | import tech.awesome.utils.extension.setAssetImage
11 |
12 | class SplashActivity : AppCompatActivity() {
13 | private val pref by inject()
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | val isFirstTime = pref.getFirstTime()
18 | if (isFirstTime) {
19 | pref.setFirstTime(false)
20 | pref.setColorMode(true)
21 | }
22 |
23 | val isNightMode = pref.getColorMode()
24 | if (isNightMode) {
25 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
26 | } else {
27 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
28 | }
29 | recreate()
30 | }
31 |
32 | override fun onStart() {
33 | super.onStart()
34 | startActivity(MainActivity.getIntent(this))
35 | finish()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Corona Track
3 | Today\'s Report
4 | Confirmed
5 | 123.123
6 | Recovered
7 | Deaths
8 | Data source:\nhttps://covid19.mathdro.id/api
9 | Map
10 | Read daily updates
11 | Daily Updates
12 | Add Country
13 | Country, City, Province…
14 | Region
15 | Country
16 | Select Country
17 | Track COVID-19 status by country
18 | Keep clean
19 | Have symptoms?
20 | Better ask doctor if you getting this
21 |
22 |
23 |
--------------------------------------------------------------------------------
/domain/src/main/java/tech/awesome/domain/network/IRepository.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.domain.network
2 |
3 | import tech.awesome.network.Api
4 | import tech.awesome.utils.Either
5 | import tech.awesome.utils.runService
6 | import tech.awesome.data.network.Confirmed
7 | import tech.awesome.data.network.Countries
8 | import tech.awesome.data.network.Daily
9 | import tech.awesome.data.network.DailySummary
10 | import tech.awesome.data.network.Overview
11 |
12 |
13 | class IRepository(private val api: Api) :
14 | Repository {
15 | override suspend fun overview(): Either = runService(api.overview())
16 |
17 | override suspend fun overviewCountry(country: String): Either =
18 | runService(api.overviewCountry(country))
19 |
20 | override suspend fun confirmed(): Either> =
21 | runService(api.confirmed())
22 |
23 | override suspend fun confirmedCountry(country: String): Either> =
24 | runService(api.confirmedCountry(country))
25 |
26 | override suspend fun dailySummary(): Either> = runService(api.dailySummary())
27 |
28 | override suspend fun daily(date: String): Either> = runService(api.daily(date))
29 |
30 | override suspend fun countries(): Either = runService(api.countries())
31 | }
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | android {
7 | compileSdkVersion 29
8 | buildToolsVersion "29.0.3"
9 |
10 | compileOptions {
11 | sourceCompatibility JavaVersion.VERSION_1_8
12 | targetCompatibility JavaVersion.VERSION_1_8
13 | }
14 |
15 | kotlinOptions {
16 | jvmTarget = JavaVersion.VERSION_1_8.toString()
17 | }
18 |
19 | defaultConfig {
20 | minSdkVersion 15
21 | targetSdkVersion 29
22 | versionCode 1
23 | versionName "1.0"
24 |
25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
26 | consumerProguardFiles 'consumer-rules.pro'
27 | }
28 |
29 | buildTypes {
30 | release {
31 | minifyEnabled false
32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33 | }
34 | }
35 |
36 | }
37 |
38 | dependencies {
39 | implementation fileTree(dir: 'libs', include: ['*.jar'])
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
41 | implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
42 | implementation 'androidx.core:core-ktx:1.2.0'
43 | testImplementation 'junit:junit:4.12'
44 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
46 |
47 | implementation project(":utils")
48 | implementation project(":network")
49 | implementation project(":data")
50 | }
51 |
--------------------------------------------------------------------------------
/network/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | android {
7 | compileSdkVersion 29
8 | buildToolsVersion "29.0.3"
9 |
10 | compileOptions {
11 | sourceCompatibility JavaVersion.VERSION_1_8
12 | targetCompatibility JavaVersion.VERSION_1_8
13 | }
14 |
15 | kotlinOptions {
16 | jvmTarget = JavaVersion.VERSION_1_8.toString()
17 | }
18 |
19 | defaultConfig {
20 | minSdkVersion 15
21 | targetSdkVersion 29
22 | versionCode 1
23 | versionName "1.0"
24 |
25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
26 | consumerProguardFiles 'consumer-rules.pro'
27 | }
28 |
29 | buildTypes {
30 | release {
31 | minifyEnabled false
32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33 | }
34 | }
35 |
36 | }
37 |
38 | dependencies {
39 | implementation fileTree(dir: 'libs', include: ['*.jar'])
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
41 | implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
42 | implementation 'androidx.core:core-ktx:1.2.0'
43 | testImplementation 'junit:junit:4.12'
44 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
46 |
47 | implementation project(":data")
48 |
49 | // Retrofit
50 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
51 | }
52 |
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/Constant.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils
2 |
3 | object NetworkConstant {
4 | const val NETWORK_TIMEOUT = 60L
5 | const val ERROR_MESSAGE = "Cannot proceed your request, please try again later"
6 | const val OFFLINE_MESSAGE = "No connection, turn your connection active to process"
7 | }
8 |
9 | object FormatConstant {
10 | const val FORMAT_DATE_LATEST_UPDATE = "dd MMMM yyyy, HH.mm"
11 | const val FORMAT_DATE = "dd MMMM yyyy"
12 | const val FORMAT_DATE_MONTH = "MMM"
13 | const val FORMAT_DATE_SERVER_FULL = "yyyy-MM-dd'T'HH:mm:ss"
14 | const val FORMAT_DATE_SERVER_FULL_2 = "yyyy-MM-dd HH:mm:ss"
15 | const val FORMAT_DATE_SERVER = "yyyy-MM-dd"
16 | const val FORMAT_DATE_DAILY_PATH = "MM-dd-yyyy"
17 | }
18 |
19 | object IntentRequestConstant {
20 | const val ADD_COUNTRY = 0
21 | }
22 | object ExtraConstant {
23 | const val EXTRA_URL = "extra_url"
24 | const val EXTRA_OVERVIEW = "extra_overview"
25 | const val EXTRA_DAILY_SUMMARY = "extra_dailys_summary"
26 | const val EXTRA_DATE = "extra_date"
27 | const val EXTRA_TITLE = "extra_title"
28 | const val EXTRA_COUNTRY = "extra_country"
29 | const val EXTRA_CONFIRMED = "extra_confirmed"
30 | const val EXTRA_STATUS = "extra_status"
31 | const val EXTRA_LATLNG = "extra_latlng"
32 | }
33 |
34 | object PrefKey {
35 | const val PREF_NAME = "CoronaPref"
36 | const val Country = "pref_country"
37 | const val NightMode = "pref_night_mode"
38 | const val IsFirstTime = "pref_is_first_time"
39 | }
40 |
41 | object StatusConstant {
42 | const val CONFIRMED = 0
43 | const val RECOVERED = 1
44 | const val DEATHS = 2
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/binding/ViewBinding.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.binding
2 |
3 | import android.annotation.SuppressLint
4 | import android.widget.ImageView
5 | import android.widget.TextView
6 | import androidx.databinding.BindingAdapter
7 | import tech.awesome.data.network.OverviewItem
8 | import tech.awesome.utils.extension.*
9 | import java.text.NumberFormat
10 |
11 | @SuppressLint("SetTextI18n")
12 | @BindingAdapter("setLatestUpdateTime")
13 | fun TextView.setLatestUpdateTime(time: String?) {
14 | this.text = if (time != null) "Last Update: ${time.getLastUpdate()}" else ""
15 | }
16 | @BindingAdapter("setFormattedDate")
17 | fun TextView.setFormattedDate(time: String?) {
18 | this.text = if (time != null) "Last Update: ${time.getFormattedDate()}" else ""
19 | }
20 | @BindingAdapter("setFormattedDate2")
21 | fun TextView.setFormattedDate2(time: String?) {
22 | this.text = time?.getFormattedDate() ?: ""
23 | }
24 | @BindingAdapter("setFormattedDate")
25 | fun TextView.setFormattedDate(time: Long?) {
26 | this.text = if (time != null) "Last Update: ${time.formattedTime()}" else ""
27 | }
28 |
29 | @BindingAdapter("setOverviewValue")
30 | fun TextView.setOverviewValue(item: OverviewItem?) {
31 | this.text = NumberFormat.getIntegerInstance().format(item?.value ?: 0)
32 | }
33 |
34 | @BindingAdapter("setOverviewValue")
35 | fun TextView.setOverviewValue(item: Int?) {
36 | this.text = NumberFormat.getIntegerInstance().format(item ?: 0)
37 | }
38 |
39 | @BindingAdapter("setFlagImage")
40 | fun ImageView.setFlagImage(isoCode: String?) {
41 | if (!isoCode.isNullOrBlank())
42 | this.loadImage("https://www.countryflags.io/$isoCode/flat/64.png")
43 | }
--------------------------------------------------------------------------------
/domain/src/main/java/tech/awesome/domain/local/IDBRepository.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.domain.local
2 |
3 | import tech.awesome.data.local.CoronaDB
4 | import tech.awesome.data.local.entity.Country
5 | import tech.awesome.utils.Either
6 | import tech.awesome.utils.value
7 |
8 | class IDBRepository(private val db: CoronaDB) : DBRepository {
9 | override suspend fun getCountry(): Either> {
10 | return try {
11 | val result = db.countryDao().get()
12 | value(result)
13 | } catch (e: Throwable) {
14 | error(e)
15 | }
16 | }
17 |
18 | override suspend fun getCountry(name: String): Either> {
19 | return try {
20 | val result = db.countryDao().get(name)
21 | value(result)
22 | } catch (e: Throwable) {
23 | error(e)
24 | }
25 | }
26 |
27 | override suspend fun insertCountry(country: Country): Either {
28 | return try {
29 | db.countryDao().insert(country)
30 | value(true)
31 | } catch (e: Throwable) {
32 | error(e)
33 | }
34 | }
35 |
36 | override suspend fun updateCountry(country: Country): Either {
37 | return try {
38 | db.countryDao().update(country)
39 | value(true)
40 | } catch (e: Throwable) {
41 | error(e)
42 | }
43 | }
44 |
45 | override suspend fun deleteCountry(name: String): Either {
46 | return try {
47 | db.countryDao().delete(name)
48 | value(true)
49 | } catch (e: Throwable) {
50 | error(e)
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/AppCompatActivityExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import android.os.Build
7 | import android.view.inputmethod.InputMethodManager
8 | import android.widget.Toast
9 | import androidx.appcompat.app.AppCompatActivity
10 |
11 | fun AppCompatActivity.isNetworkAvailable(): Boolean {
12 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
13 | return (getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo != null
14 | } else {
15 | val nw =
16 | (getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetwork
17 | ?: return false
18 | val actNw =
19 | (getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).getNetworkCapabilities(
20 | nw
21 | )
22 | ?: return false
23 | return when {
24 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
25 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
26 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
27 | else -> false
28 | }
29 | }
30 | }
31 |
32 | fun AppCompatActivity.hideKeyboard() {
33 | val view = this.currentFocus
34 | if (view != null) {
35 | val imm =
36 | getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
37 | imm.hideSoftInputFromWindow(view.windowToken, 0)
38 | }
39 | }
40 |
41 | fun AppCompatActivity.showToast(message: String) {
42 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/widget/StatusText.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.widget
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import androidx.appcompat.widget.AppCompatTextView
7 | import androidx.core.content.ContextCompat
8 | import tech.awesome.coronatrack.R
9 | import tech.awesome.utils.StatusConstant
10 | import java.text.NumberFormat
11 |
12 | class StatusText @JvmOverloads constructor(
13 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
14 | ) : AppCompatTextView(context, attrs, defStyleAttr) {
15 | var value: Int = 0
16 | var status: Int = 0
17 | @SuppressLint("SetTextI18n")
18 | set(status) {
19 | var color = 0
20 | lateinit var textStatus: String
21 | when (status) {
22 | StatusConstant.CONFIRMED -> {
23 | color = ContextCompat.getColor(context, R.color.confirmed)
24 | textStatus = context.getString(R.string.label_confirmed)
25 | }
26 | StatusConstant.RECOVERED -> {
27 | color = ContextCompat.getColor(context, R.color.recovered)
28 | textStatus = context.getString(R.string.label_recovered)
29 | }
30 | StatusConstant.DEATHS -> {
31 | color = ContextCompat.getColor(context, R.color.death)
32 | textStatus = context.getString(R.string.label_death)
33 | }
34 | }
35 | if (color != 0) {
36 | text = "$textStatus: ${NumberFormat.getIntegerInstance().format(value)}"
37 | setTextColor(color)
38 | }
39 | field = status
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_map_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.di
2 |
3 | import com.facebook.stetho.okhttp3.StethoInterceptor
4 | import okhttp3.OkHttpClient
5 | import okhttp3.logging.HttpLoggingInterceptor
6 | import org.koin.android.ext.koin.androidContext
7 | import org.koin.dsl.module
8 | import retrofit2.Retrofit
9 | import retrofit2.converter.moshi.MoshiConverterFactory
10 | import tech.awesome.coronatrack.BuildConfig
11 | import tech.awesome.network.Api
12 | import tech.awesome.network.CacheProvider
13 | import tech.awesome.network.ICacheProvider
14 | import tech.awesome.utils.NetworkConstant
15 | import java.util.concurrent.TimeUnit
16 |
17 | val networkModule = module {
18 | single {
19 | MoshiConverterFactory.create()
20 | }
21 |
22 | single {
23 | ICacheProvider(androidContext().cacheDir) as CacheProvider
24 | }
25 |
26 | single {
27 | OkHttpClient().newBuilder()
28 | .connectTimeout(NetworkConstant.NETWORK_TIMEOUT, TimeUnit.SECONDS)
29 | .readTimeout(NetworkConstant.NETWORK_TIMEOUT, TimeUnit.SECONDS)
30 | .writeTimeout(NetworkConstant.NETWORK_TIMEOUT, TimeUnit.SECONDS)
31 | .cache(get().cache)
32 | .addInterceptor(StethoInterceptor())
33 | .addInterceptor(
34 | HttpLoggingInterceptor()
35 | .setLevel(HttpLoggingInterceptor.Level.BODY)
36 | )
37 | .addInterceptor(get().getInterceptor())
38 | .build()
39 | }
40 |
41 | single {
42 | Retrofit.Builder()
43 | .baseUrl(BuildConfig.BASE_URL)
44 | .client(get())
45 | .addConverterFactory(get())
46 | .build()
47 | }
48 |
49 | single {
50 | get().create(Api::class.java)
51 | }
52 |
53 | }
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/extension/StringExt.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils.extension
2 |
3 | import android.annotation.SuppressLint
4 | import tech.awesome.utils.FormatConstant
5 | import java.text.SimpleDateFormat
6 | import java.util.*
7 |
8 | fun String?.isContains(comparedTo: String): Boolean =
9 | this.toString().toLowerCase(Locale.getDefault()).contains(
10 | comparedTo.toLowerCase(
11 | Locale.getDefault()
12 | )
13 | )
14 |
15 | @SuppressLint("SimpleDateFormat")
16 | fun String?.getLastUpdate(): String {
17 | if (this.isNullOrBlank()) return ""
18 | val parser =
19 | SimpleDateFormat(if (this.contains("T")) FormatConstant.FORMAT_DATE_SERVER_FULL else FormatConstant.FORMAT_DATE_SERVER_FULL_2)
20 | val formatter = SimpleDateFormat(FormatConstant.FORMAT_DATE_LATEST_UPDATE)
21 | return formatter.format(parser.parse(this) ?: "")
22 | }
23 |
24 | @SuppressLint("SimpleDateFormat")
25 | fun String?.getFormattedDate(): String {
26 | if (this.isNullOrBlank()) return ""
27 | val parser = SimpleDateFormat(FormatConstant.FORMAT_DATE_SERVER)
28 | val formatter = SimpleDateFormat(FormatConstant.FORMAT_DATE)
29 | return formatter.format(parser.parse(this) ?: "")
30 | }
31 |
32 | @SuppressLint("SimpleDateFormat")
33 | fun String?.getDailyPathDate(): String {
34 | if (this.isNullOrBlank()) return ""
35 | val parser = SimpleDateFormat(FormatConstant.FORMAT_DATE_SERVER)
36 | val formatter = SimpleDateFormat(FormatConstant.FORMAT_DATE_DAILY_PATH)
37 | return formatter.format(parser.parse(this) ?: "")
38 | }
39 |
40 | @SuppressLint("SimpleDateFormat")
41 | fun String?.getMonth(): String {
42 | if (this.isNullOrBlank()) return ""
43 | val parser = SimpleDateFormat(FormatConstant.FORMAT_DATE_SERVER)
44 | val formatter = SimpleDateFormat(FormatConstant.FORMAT_DATE_MONTH)
45 | return formatter.format(parser.parse(this) ?: "")
46 | }
--------------------------------------------------------------------------------
/utils/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | android {
7 | compileSdkVersion 29
8 | buildToolsVersion "29.0.3"
9 |
10 | compileOptions {
11 | sourceCompatibility JavaVersion.VERSION_1_8
12 | targetCompatibility JavaVersion.VERSION_1_8
13 | }
14 |
15 | kotlinOptions {
16 | jvmTarget = JavaVersion.VERSION_1_8.toString()
17 | }
18 |
19 | defaultConfig {
20 | minSdkVersion 15
21 | targetSdkVersion 29
22 | versionCode 1
23 | versionName "1.0"
24 |
25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
26 | consumerProguardFiles 'consumer-rules.pro'
27 | }
28 |
29 | buildTypes {
30 | release {
31 | minifyEnabled false
32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33 | }
34 | }
35 |
36 | }
37 |
38 | dependencies {
39 | implementation fileTree(dir: 'libs', include: ['*.jar'])
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
41 | implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
42 | implementation 'androidx.core:core-ktx:1.2.0'
43 | testImplementation 'junit:junit:4.12'
44 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
46 |
47 | // Retrofit
48 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
49 |
50 | // Coil
51 | implementation("io.coil-kt:coil:0.9.5")
52 |
53 | // Facebook Shimmering
54 | implementation "com.facebook.shimmer:shimmer:$facebook_shimmer"
55 |
56 | // GSON
57 | implementation "com.google.code.gson:gson:$gson"
58 | implementation "com.google.guava:guava:$guava"
59 | }
60 |
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | android {
7 | compileSdkVersion 29
8 | buildToolsVersion "29.0.3"
9 |
10 | compileOptions {
11 | sourceCompatibility JavaVersion.VERSION_1_8
12 | targetCompatibility JavaVersion.VERSION_1_8
13 | }
14 |
15 | kotlinOptions {
16 | jvmTarget = JavaVersion.VERSION_1_8.toString()
17 | }
18 |
19 | defaultConfig {
20 | minSdkVersion 15
21 | targetSdkVersion 29
22 | versionCode 1
23 | versionName "1.0"
24 |
25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
26 | consumerProguardFiles 'consumer-rules.pro'
27 | }
28 |
29 | buildTypes {
30 | release {
31 | minifyEnabled false
32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33 | }
34 | }
35 |
36 | }
37 |
38 | dependencies {
39 | implementation fileTree(dir: 'libs', include: ['*.jar'])
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
41 | implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
42 | implementation 'androidx.core:core-ktx:1.2.0'
43 | testImplementation 'junit:junit:4.12'
44 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
46 |
47 | // Room
48 | api "androidx.room:room-runtime:$room"
49 | annotationProcessor "androidx.room:room-compiler:$room"
50 | kapt "androidx.room:room-compiler:$room"
51 | implementation "androidx.room:room-ktx:$room"
52 | androidTestImplementation "androidx.room:room-testing:$room"
53 |
54 | // Moshi
55 | implementation "com.squareup.moshi:moshi:$moshiVersion"
56 | kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion"
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/maps/MapsViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.maps
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.launch
7 | import tech.awesome.coronatrack.ui.base.BindingViewModel
8 | import tech.awesome.data.network.Confirmed
9 | import tech.awesome.data.network.Countries
10 | import tech.awesome.domain.local.DBRepository
11 | import tech.awesome.domain.network.Repository
12 | import tech.awesome.utils.Either
13 | import tech.awesome.utils.State
14 | import tech.awesome.utils.extension.getError
15 |
16 | class MapsViewModel(
17 | private val repository: Repository,
18 | private val dbRepository: DBRepository
19 | ) : BindingViewModel() {
20 |
21 | private val _confirmedState = MutableLiveData>>()
22 | val confirmedState: LiveData>> get() = _confirmedState
23 |
24 | private val _countryItemState = MutableLiveData>()
25 | val countryState: LiveData> get() = _countryItemState
26 |
27 | fun getConfirmed() {
28 | viewModelScope.launch {
29 | _confirmedState.postValue(State.Loading)
30 | when (val result = repository.confirmed()) {
31 | is Either.Success -> _confirmedState.postValue(State.Success(result.value))
32 | is Either.Failure -> _confirmedState.postValue(State.Error(result.error.getError()))
33 | }
34 | }
35 | }
36 |
37 | fun getCountries() {
38 | viewModelScope.launch {
39 | _countryItemState.postValue(State.Loading)
40 | when (val result = repository.countries()) {
41 | is Either.Success -> _countryItemState.postValue(State.Success(result.value))
42 | is Either.Failure -> _countryItemState.postValue(State.Error(result.error.getError()))
43 | }
44 | }
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/Application.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack
2 |
3 | import android.content.Context
4 | import androidx.multidex.MultiDex
5 | import androidx.multidex.MultiDexApplication
6 | import com.facebook.stetho.Stetho
7 | import io.github.inflationx.calligraphy3.CalligraphyConfig
8 | import io.github.inflationx.calligraphy3.CalligraphyInterceptor
9 | import io.github.inflationx.viewpump.ViewPump
10 | import org.koin.android.ext.koin.androidContext
11 | import org.koin.android.ext.koin.androidLogger
12 | import org.koin.core.context.startKoin
13 | import org.koin.core.logger.Level
14 | import tech.awesome.coronatrack.di.*
15 | import tech.awesome.utils.Saver
16 | import timber.log.Timber
17 |
18 | class Application : MultiDexApplication() {
19 | // private val viewPump: ViewPump by inject()
20 |
21 | override fun onCreate() {
22 | super.onCreate()
23 | if (BuildConfig.DEBUG) {
24 | Timber.plant(Timber.DebugTree())
25 | Stetho.initializeWithDefaults(this)
26 | }
27 |
28 | Saver.init(this)
29 |
30 | startKoin {
31 | androidLogger(Level.INFO)
32 | androidContext(this@Application)
33 | modules(
34 | listOf(
35 | appModule,
36 | networkModule,
37 | persistenceModule,
38 | repositoryModule,
39 | viewModelModule
40 | )
41 | )
42 | }
43 | ViewPump.init(
44 | ViewPump.builder()
45 | .addInterceptor(
46 | CalligraphyInterceptor(
47 | CalligraphyConfig.Builder()
48 | .setDefaultFontPath("fonts/Menlo-Regular.ttf")
49 | .setFontAttrId(R.attr.fontPath)
50 | .build()
51 | )
52 | )
53 | .build()
54 | )
55 | }
56 |
57 |
58 | override fun attachBaseContext(base: Context) {
59 | super.attachBaseContext(base)
60 | MultiDex.install(this)
61 | }
62 |
63 | companion object {
64 | operator fun get(context: Context): Application {
65 | return context.applicationContext as Application
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
8 |
11 |
14 |
17 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/daily_updates/DailyUpdatesViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.daily_updates
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.launch
7 | import tech.awesome.domain.local.DBRepository
8 | import tech.awesome.domain.network.Repository
9 | import tech.awesome.coronatrack.ui.base.BindingViewModel
10 | import tech.awesome.data.network.Countries
11 | import tech.awesome.data.network.Daily
12 | import tech.awesome.data.network.DailySummary
13 | import tech.awesome.utils.*
14 | import tech.awesome.utils.extension.getError
15 |
16 | class DailyUpdatesViewModel(
17 | private val repository: Repository,
18 | private val dbRepository: DBRepository
19 | ) : BindingViewModel() {
20 |
21 | private val _dailySummaryState = MutableLiveData>>()
22 | val dailySummaryState: LiveData>> get() = _dailySummaryState
23 |
24 | private val _dailyState = MutableLiveData>>()
25 | val dailyState: LiveData>> get() = _dailyState
26 |
27 | private val _countryItemState = MutableLiveData>()
28 | val countryState: LiveData> get() = _countryItemState
29 |
30 | fun getDaily(date: String) {
31 | viewModelScope.launch {
32 | _dailyState.postValue(State.Loading)
33 | when (val result = repository.daily(date)) {
34 | is Either.Success -> _dailyState.postValue(State.Success(result.value))
35 | is Either.Failure -> _dailyState.postValue(State.Error(result.error.getError()))
36 | }
37 | }
38 | }
39 |
40 | fun getDailySummary() {
41 | viewModelScope.launch {
42 | _dailySummaryState.postValue(State.Loading)
43 | when (val result = repository.dailySummary()) {
44 | is Either.Success -> _dailySummaryState.postValue(State.Success(result.value))
45 | is Either.Failure -> _dailySummaryState.postValue(State.Error(result.error.getError()))
46 | }
47 | }
48 | }
49 |
50 | fun getCountries() {
51 | viewModelScope.launch {
52 | _countryItemState.postValue(State.Loading)
53 | when (val result = repository.countries()) {
54 | is Either.Success -> _countryItemState.postValue(State.Success(result.value))
55 | is Either.Failure -> _countryItemState.postValue(State.Error(result.error.getError()))
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_webview.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
23 |
24 |
34 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
57 |
58 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_region.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
21 |
22 |
28 |
29 |
48 |
49 |
50 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/add_country/AddCountryViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.add_country
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.launch
7 | import tech.awesome.coronatrack.ui.base.BindingViewModel
8 | import tech.awesome.data.local.entity.Country
9 | import tech.awesome.data.network.Confirmed
10 | import tech.awesome.data.network.Countries
11 | import tech.awesome.domain.local.DBRepository
12 | import tech.awesome.domain.network.Repository
13 | import tech.awesome.utils.Either
14 | import tech.awesome.utils.State
15 | import tech.awesome.utils.extension.getError
16 |
17 | class AddCountryViewModel(
18 | private val repository: Repository,
19 | private val dbRepository: DBRepository
20 | ) : BindingViewModel() {
21 |
22 | private val _confirmedState = MutableLiveData>>()
23 | val confirmedState: LiveData>> get() = _confirmedState
24 |
25 | private val _countryItemState = MutableLiveData>()
26 | val countryState: LiveData> get() = _countryItemState
27 |
28 | private val _countryDBState = MutableLiveData>()
29 | val countryDBState: LiveData> get() = _countryDBState
30 |
31 | fun getConfirmed() {
32 | viewModelScope.launch {
33 | _confirmedState.postValue(State.Loading)
34 | when (val result = repository.confirmed()) {
35 | is Either.Success -> _confirmedState.postValue(State.Success(result.value))
36 | is Either.Failure -> _confirmedState.postValue(State.Error(result.error.getError()))
37 | }
38 | }
39 | }
40 |
41 |
42 | fun getCountries() {
43 | viewModelScope.launch {
44 | _countryItemState.postValue(State.Loading)
45 | when (val result = repository.countries()) {
46 | is Either.Success -> _countryItemState.postValue(State.Success(result.value))
47 | is Either.Failure -> _countryItemState.postValue(State.Error(result.error.getError()))
48 | }
49 | }
50 | }
51 |
52 | fun setCountryDB(country: Country) {
53 | viewModelScope.launch {
54 | _countryDBState.postValue(State.Loading)
55 | when (val result = dbRepository.insertCountry(country)) {
56 | is Either.Success -> {
57 | when (result.value) {
58 | true -> _countryDBState.postValue(State.Success(country))
59 | false -> _countryDBState.postValue(State.Error("Data is not found"))
60 | }
61 | }
62 | is Either.Failure -> _countryDBState.postValue(State.Error(result.error.getError()))
63 | }
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_region_selected.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
22 |
23 |
29 |
30 |
49 |
50 |
51 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
11 |
12 |
13 |
23 |
26 |
34 |
37 |
38 |
42 |
45 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_daily_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
23 |
24 |
33 |
34 |
48 |
49 |
60 |
61 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/webview/WebviewActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.webview
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Build
7 | import android.os.Bundle
8 | import android.view.KeyEvent
9 | import android.webkit.WebChromeClient
10 | import android.webkit.WebView
11 | import android.webkit.WebViewClient
12 | import kotlinx.android.synthetic.main.activity_webview.*
13 | import tech.awesome.coronatrack.R
14 | import tech.awesome.coronatrack.ui.base.BaseActivity
15 | import tech.awesome.utils.ExtraConstant
16 | import tech.awesome.utils.extension.gone
17 | import tech.awesome.utils.extension.visible
18 |
19 | class WebviewActivity : BaseActivity() {
20 |
21 | companion object {
22 | fun getIntent(context: Context?, url: String, title: String) : Intent =
23 | Intent(context, WebviewActivity::class.java).apply {
24 | putExtra(ExtraConstant.EXTRA_URL, url)
25 | putExtra(ExtraConstant.EXTRA_TITLE, title)
26 | }
27 | }
28 |
29 | @SuppressLint("SetJavaScriptEnabled")
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 | setContentView(R.layout.activity_webview)
33 | setupToolbar(toolbar, true)
34 |
35 | val title = intent.getStringExtra(ExtraConstant.EXTRA_TITLE) ?: ""
36 | val url = intent.getStringExtra(ExtraConstant.EXTRA_URL)
37 |
38 | tv_title.text = title
39 |
40 | webview.apply {
41 | loadUrl(if (!url.isNullOrBlank()) url else "about:blank")
42 | settings.apply {
43 | javaScriptEnabled = true
44 | domStorageEnabled = true
45 | allowContentAccess = true
46 | useWideViewPort = true
47 | allowContentAccess = true
48 | allowFileAccess = true
49 | javaScriptCanOpenWindowsAutomatically = true
50 | }
51 |
52 | webViewClient = WebViewClient()
53 | webChromeClient = object : WebChromeClient() {
54 | override fun onProgressChanged(view: WebView?, newProgress: Int) {
55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
56 | pb.setProgress(newProgress, true)
57 | } else {
58 | pb.progress = newProgress
59 | }
60 |
61 | if (newProgress == 100) {
62 | pb.gone()
63 | pb.progress = 0
64 | } else {
65 | pb.visible()
66 | }
67 | super.onProgressChanged(view, newProgress)
68 | }
69 | }
70 | }
71 |
72 |
73 | }
74 |
75 | override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
76 | if (event.action == KeyEvent.ACTION_DOWN) {
77 | when (keyCode) {
78 | KeyEvent.KEYCODE_BACK -> {
79 | if (webview.canGoBack()) {
80 | webview.goBack()
81 | } else {
82 | finish()
83 | }
84 | return true
85 | }
86 | }
87 |
88 | }
89 | return super.onKeyDown(keyCode, event)
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_maps_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
24 |
25 |
34 |
35 |
49 |
50 |
61 |
62 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.main
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.launch
7 | import tech.awesome.domain.local.DBRepository
8 | import tech.awesome.domain.network.Repository
9 | import tech.awesome.coronatrack.ui.base.BindingViewModel
10 | import tech.awesome.data.local.entity.Country
11 | import tech.awesome.data.network.DailySummary
12 | import tech.awesome.data.network.Overview
13 | import tech.awesome.utils.*
14 | import tech.awesome.utils.extension.getError
15 |
16 | class MainViewModel(private val repository: Repository, private val dbRepository: DBRepository) :
17 | BindingViewModel() {
18 | private val _overviewState = MutableLiveData>()
19 | val overviewState: LiveData> get() = _overviewState
20 |
21 | private val _dailyState = MutableLiveData>>()
22 | val dailySummaryState: LiveData>> get() = _dailyState
23 |
24 | private val _overviewCountryState = MutableLiveData>()
25 | val overviewCountryState: LiveData> get() = _overviewCountryState
26 |
27 | private val _countryDBState = MutableLiveData>()
28 | val countryDBState: LiveData> get() = _countryDBState
29 |
30 |
31 | fun getOverview() {
32 | viewModelScope.launch {
33 | _overviewState.postValue(State.Loading)
34 | when (val result = repository.overview()) {
35 | is Either.Success -> _overviewState.postValue(State.Success(result.value))
36 | is Either.Failure -> _overviewState.postValue(State.Error(result.error.getError()))
37 | }
38 | }
39 | }
40 |
41 | fun getOverviewCountry(countryName: String) {
42 | if (countryName.isBlank()) return
43 | viewModelScope.launch {
44 | _overviewCountryState.postValue(State.Loading)
45 | when (val result = repository.overviewCountry(countryName)) {
46 | is Either.Success -> _overviewCountryState.postValue(State.Success(result.value))
47 | is Either.Failure -> _overviewCountryState.postValue(State.Error(result.error.getError()))
48 | }
49 | }
50 | }
51 |
52 | fun getDaily() {
53 | viewModelScope.launch {
54 | _dailyState.postValue(State.Loading)
55 | when (val result = repository.dailySummary()) {
56 | is Either.Success -> _dailyState.postValue(State.Success(result.value))
57 | is Either.Failure -> _dailyState.postValue(State.Error(result.error.getError()))
58 | }
59 | }
60 | }
61 |
62 | fun getCountryDB(countryName: String) {
63 | viewModelScope.launch {
64 | _countryDBState.postValue(State.Loading)
65 | when (val result = dbRepository.getCountry(countryName)) {
66 | is Either.Success -> {
67 | if (result.value.isEmpty()) _countryDBState.postValue(State.Error("Failed to retrieve country in db"))
68 | else _countryDBState.postValue(State.Success(result.value[0]))
69 | }
70 | is Either.Failure -> _countryDBState.postValue(State.Error(result.error.getError()))
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/maps/fragment/adapter/MapsAdapter.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.maps.fragment.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import tech.awesome.coronatrack.databinding.ItemMapsContentBinding
7 | import tech.awesome.data.network.Confirmed
8 | import tech.awesome.data.network.Countries
9 | import tech.awesome.utils.StatusConstant
10 | import tech.awesome.utils.extension.isContains
11 |
12 | class MapsAdapter(private val listener: MapsListener, private var status: Int) :
13 | RecyclerView.Adapter() {
14 | private var mData = emptyArray()
15 | private var mFilteredData = emptyArray()
16 | private var countries: Countries? = null
17 | private var selectedIndex = -1
18 |
19 | interface MapsListener {
20 | fun onClickItem(confirmed: Confirmed, index: Int)
21 | }
22 |
23 | inner class MapsViewHolder(val binding: ItemMapsContentBinding) :
24 | RecyclerView.ViewHolder(binding.root)
25 |
26 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MapsViewHolder {
27 | return MapsViewHolder(
28 | ItemMapsContentBinding.inflate(
29 | LayoutInflater.from(parent.context),
30 | parent,
31 | false
32 | )
33 | )
34 | }
35 |
36 | override fun getItemCount(): Int = mFilteredData.size
37 |
38 | override fun onBindViewHolder(holder: MapsViewHolder, position: Int) {
39 | holder.binding.clMaps.setOnClickListener { listener.onClickItem(mFilteredData[position], position) }
40 | holder.binding.confirmed = mFilteredData[position]
41 | val value =
42 | when (status) {
43 | StatusConstant.CONFIRMED -> mFilteredData[position].confirmed ?: 0
44 | StatusConstant.RECOVERED -> mFilteredData[position].recovered ?: 0
45 | else -> mFilteredData[position].deaths ?: 0
46 | }
47 |
48 | holder.binding.tvContentValue.value = value
49 | holder.binding.tvContentValue.status = this.status
50 | if (countries != null) {
51 | mFilteredData[position].countryRegion?.let {
52 | val filteredCountry =
53 | countries?.countries?.filter { item -> item.name.isContains(it) }
54 | if (!filteredCountry.isNullOrEmpty())
55 | holder.binding.country = filteredCountry[0]
56 | }
57 | }
58 | }
59 |
60 | fun setData(data: Array) {
61 | mData =
62 | data.distinctBy { if (it.provinceState.isNullOrBlank()) it.countryRegion else it.provinceState }
63 | .sortedBy { if (it.provinceState.isNullOrBlank()) it.countryRegion else it.provinceState }
64 | .toTypedArray()
65 | mFilteredData = mData.copyOf()
66 | notifyDataSetChanged()
67 | }
68 |
69 | fun setCountries(country: Countries) {
70 | countries = country
71 | notifyDataSetChanged()
72 | }
73 |
74 | fun setStatus(status: Int) {
75 | this.status = status
76 | notifyDataSetChanged()
77 | }
78 |
79 | fun getStatus() = status
80 |
81 | fun setSelected(index: Int) {
82 | val prevIndex = selectedIndex
83 | selectedIndex = index
84 |
85 | if (prevIndex != -1) notifyItemChanged(prevIndex)
86 | notifyItemChanged(selectedIndex)
87 | }
88 |
89 | fun setShownData(keyword: String?) {
90 | mFilteredData = if (keyword.isNullOrBlank()) {
91 | mData.copyOf()
92 | } else {
93 | mData.filter { data ->
94 | data.provinceState.isContains(keyword) || data.countryRegion.isContains(keyword)
95 | }.toTypedArray()
96 | }
97 | notifyDataSetChanged()
98 | }
99 | }
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | xmlns:android
17 |
18 | ^$
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | xmlns:.*
28 |
29 | ^$
30 |
31 |
32 | BY_NAME
33 |
34 |
35 |
36 |
37 |
38 |
39 | .*:id
40 |
41 | http://schemas.android.com/apk/res/android
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | .*:name
51 |
52 | http://schemas.android.com/apk/res/android
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | name
62 |
63 | ^$
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | style
73 |
74 | ^$
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | .*
84 |
85 | ^$
86 |
87 |
88 | BY_NAME
89 |
90 |
91 |
92 |
93 |
94 |
95 | .*
96 |
97 | http://schemas.android.com/apk/res/android
98 |
99 |
100 | ANDROID_ATTRIBUTE_ORDER
101 |
102 |
103 |
104 |
105 |
106 |
107 | .*
108 |
109 | .*
110 |
111 |
112 | BY_NAME
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/add_country/adapter/AddCountryAdapter.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.add_country.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import tech.awesome.coronatrack.databinding.ItemRegionBinding
7 | import tech.awesome.coronatrack.databinding.ItemRegionSelectedBinding
8 | import tech.awesome.data.network.Confirmed
9 | import tech.awesome.utils.extension.isContains
10 |
11 | class AddCountryAdapter(private val listener: AddCountryListener) :
12 | RecyclerView.Adapter() {
13 | private var mData = emptyArray()
14 | private var mFilteredData = emptyArray()
15 | private var selectedIndex = -1
16 |
17 | interface AddCountryListener {
18 | fun onClickItem(confirmed: Confirmed, index: Int)
19 | }
20 |
21 | inner class AddCountryViewHolder(val binding: ItemRegionBinding) :
22 | RecyclerView.ViewHolder(binding.root)
23 |
24 | inner class AddCountrySelectedViewHolder(val binding: ItemRegionSelectedBinding) :
25 | RecyclerView.ViewHolder(binding.root)
26 |
27 | override fun getItemViewType(position: Int): Int {
28 | if (position == selectedIndex) return 1
29 | return super.getItemViewType(position)
30 | }
31 |
32 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
33 | if (viewType == 1)
34 | return AddCountrySelectedViewHolder(
35 | ItemRegionSelectedBinding.inflate(
36 | LayoutInflater.from(parent.context),
37 | parent,
38 | false
39 | )
40 | )
41 | return AddCountryViewHolder(
42 | ItemRegionBinding.inflate(
43 | LayoutInflater.from(parent.context),
44 | parent,
45 | false
46 | )
47 | )
48 | }
49 |
50 | override fun getItemCount(): Int = mFilteredData.size
51 |
52 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
53 | when (holder) {
54 | is AddCountryViewHolder -> {
55 | mFilteredData[position].apply {
56 | holder.binding.clCountry.setOnClickListener {
57 | listener.onClickItem(
58 | this,
59 | position
60 | )
61 | }
62 | holder.binding.region = provinceState ?: countryRegion
63 | holder.binding.country = countryRegion
64 | }
65 | }
66 | is AddCountrySelectedViewHolder -> {
67 | mFilteredData[position].apply {
68 | holder.binding.clCountrySelected.setOnClickListener {
69 | listener.onClickItem(
70 | this,
71 | position
72 | )
73 | }
74 | holder.binding.region = provinceState ?: countryRegion
75 | holder.binding.country = countryRegion
76 | }
77 | }
78 | }
79 | }
80 |
81 | fun setData(data: Array) {
82 | mData =
83 | data.distinctBy { if (it.provinceState.isNullOrBlank()) it.countryRegion else it.provinceState }
84 | .sortedBy { if (it.provinceState.isNullOrBlank()) it.countryRegion else it.provinceState }
85 | .toTypedArray()
86 | mFilteredData = mData.copyOf()
87 | notifyDataSetChanged()
88 | }
89 |
90 | fun setSelected(index: Int) {
91 | val prevIndex = selectedIndex
92 | selectedIndex = index
93 |
94 | if (prevIndex != -1) notifyItemChanged(prevIndex)
95 | notifyItemChanged(selectedIndex)
96 | }
97 |
98 | fun setShownData(keyword: String?) {
99 | mFilteredData = if (keyword.isNullOrBlank()) {
100 | mData.copyOf()
101 | } else {
102 | mData.filter { data ->
103 | data.provinceState.isContains(keyword) || data.countryRegion.isContains(keyword)
104 | }.toTypedArray()
105 | }
106 | notifyDataSetChanged()
107 | }
108 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | android {
7 | compileSdkVersion 29
8 | buildToolsVersion "29.0.3"
9 |
10 | compileOptions {
11 | sourceCompatibility JavaVersion.VERSION_1_8
12 | targetCompatibility JavaVersion.VERSION_1_8
13 | }
14 |
15 | kotlinOptions {
16 | jvmTarget = JavaVersion.VERSION_1_8.toString()
17 | }
18 |
19 | defaultConfig {
20 | applicationId "tech.awesome.coronatrack"
21 | minSdkVersion 15
22 | targetSdkVersion 29
23 | versionCode 1
24 | versionName "1.0"
25 | buildConfigField("String", "BASE_URL", "\"https://covid19.mathdro.id/\"")
26 | buildConfigField("String", "BASE_URL_COVID19", "\"https://api.covid19api.com/\"")
27 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
28 | multiDexEnabled true
29 | }
30 |
31 | buildTypes {
32 | debug {
33 | applicationIdSuffix ".dev"
34 | }
35 | release {
36 | minifyEnabled false
37 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
38 | }
39 | }
40 |
41 | dataBinding {
42 | enabled = true
43 | }
44 |
45 | }
46 |
47 | dependencies {
48 | implementation fileTree(dir: 'libs', include: ['*.jar'])
49 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
50 | implementation "androidx.appcompat:appcompat:$appcompat"
51 | implementation "com.google.android.material:material:$material_version"
52 | implementation "androidx.core:core-ktx:$coreKtx"
53 | implementation "androidx.constraintlayout:constraintlayout:$constraint_version"
54 | implementation "androidx.recyclerview:recyclerview:$rvVersion"
55 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
56 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle"
57 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle"
58 |
59 | // Testing
60 | testImplementation "junit:junit:$junit"
61 | androidTestImplementation "androidx.test.ext:junit:$junitExt"
62 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso"
63 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
64 | testImplementation "android.arch.core:core-testing:$archCore"
65 | testImplementation "org.mockito:mockito-core:$mockito"
66 | testImplementation "org.mockito:mockito-inline:$mockito"
67 |
68 | implementation project(":utils")
69 | implementation project(":network")
70 | implementation project(":data")
71 | implementation project(":domain")
72 |
73 | // MPA Android Chart
74 | implementation "com.github.PhilJay:MPAndroidChart:v$mpa_chart"
75 |
76 | // Koin
77 | implementation "org.koin:koin-android:$koin_version"
78 | implementation "org.koin:koin-android-scope:$koin_version"
79 | implementation "org.koin:koin-android-viewmodel:$koin_version"
80 |
81 | // Architecture components
82 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
83 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
84 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
85 |
86 | // Multidex
87 | implementation "androidx.multidex:multidex:$multidex"
88 |
89 | // Coroutines
90 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
91 |
92 | // Maps
93 | implementation "com.google.android.gms:play-services-maps:$maps"
94 |
95 | // Calligraphy
96 | implementation "io.github.inflationx:calligraphy3:$calligraphy"
97 | implementation "io.github.inflationx:viewpump:$viewpump"
98 |
99 | // Stetho
100 | implementation "com.facebook.stetho:stetho:$stetho"
101 | implementation "com.facebook.stetho:stetho-okhttp3:$stetho"
102 |
103 | // Timber
104 | implementation "com.jakewharton.timber:timber:$timber"
105 |
106 | // Moshi
107 | implementation "com.squareup.retrofit2:converter-moshi:$converter_moshi_version"
108 |
109 | // Retrofit
110 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
111 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
112 |
113 | // Facebook Shimmering
114 | implementation "com.facebook.shimmer:shimmer:$facebook_shimmer"
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/daily_updates/adapter/DailyUpdatesAdapter.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.daily_updates.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import tech.awesome.data.network.Countries
7 | import tech.awesome.data.network.Daily
8 | import tech.awesome.coronatrack.databinding.ItemDailyContentBinding
9 | import tech.awesome.coronatrack.databinding.ItemDailyLoadingBinding
10 | import tech.awesome.utils.StatusConstant
11 | import tech.awesome.utils.extension.isContains
12 |
13 | class DailyUpdatesAdapter(private var status: Int) :
14 | RecyclerView.Adapter() {
15 | private var dailys = emptyArray()
16 | private var countries: Countries? = null
17 |
18 | private var page: Int = 1
19 | private var numPerPage: Int = 10
20 |
21 | var isLastPage = true
22 | private var shownPage =
23 | if (dailys.size > (page * numPerPage)) (page * numPerPage) + 1
24 | else dailys.size
25 |
26 | override fun getItemViewType(position: Int): Int {
27 | if (!isLastPage && position == shownPage - 1) return 1
28 | return super.getItemViewType(position)
29 | }
30 |
31 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
32 | if (viewType == 1) DailyUpdatesLoadingViewHolder(
33 | ItemDailyLoadingBinding.inflate(
34 | LayoutInflater.from(parent.context),
35 | parent,
36 | false
37 | )
38 | ) else
39 | DailyUpdatesViewHolder(
40 | ItemDailyContentBinding.inflate(
41 | LayoutInflater.from(parent.context),
42 | parent,
43 | false
44 | )
45 | )
46 |
47 | override fun getItemCount(): Int = shownPage
48 |
49 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
50 | when (holder) {
51 | is DailyUpdatesViewHolder -> {
52 | holder.binding.daily = dailys[position]
53 | val value =
54 | when (status) {
55 | StatusConstant.CONFIRMED -> dailys[position].confirmed
56 | StatusConstant.RECOVERED -> dailys[position].recovered
57 | else -> dailys[position].deaths
58 | }
59 |
60 | holder.binding.tvContentValue.value = value
61 | holder.binding.tvContentValue.status = this.status
62 | if (countries != null) {
63 | dailys[position].countryRegion?.let {
64 | val filteredCountry =
65 | countries?.countries?.filter { item -> item.name.isContains(it) }
66 | if (!filteredCountry.isNullOrEmpty())
67 | holder.binding.country = filteredCountry[0]
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | fun setDailys(newDailys: Array) {
75 | dailys = newDailys
76 | updateShownPage()
77 | notifyDataSetChanged()
78 | }
79 |
80 | fun setCountries(country: Countries) {
81 | countries = country
82 | resetShownPage()
83 | notifyDataSetChanged()
84 | }
85 |
86 | fun setStatus(status: Int) {
87 | this.status = status
88 | resetShownPage()
89 | notifyDataSetChanged()
90 | }
91 |
92 | fun getStatus() = status
93 |
94 | private fun resetShownPage() {
95 | page = 1
96 | updateShownPage()
97 | }
98 |
99 | fun loadMore() {
100 | val currentLastIndex = shownPage - 1
101 | page += 1
102 | updateShownPage()
103 | notifyItemChanged(currentLastIndex)
104 | notifyItemRangeInserted(
105 | currentLastIndex,
106 | (shownPage).coerceAtMost(dailys.size) - 1
107 | )
108 | }
109 |
110 | private fun updateShownPage() {
111 | if (dailys.size > (page * numPerPage)) {
112 | isLastPage = false
113 | shownPage = (page * numPerPage) + 1
114 | } else {
115 | isLastPage = true
116 | shownPage = dailys.size
117 | }
118 | }
119 |
120 | inner class DailyUpdatesViewHolder(val binding: ItemDailyContentBinding) :
121 | RecyclerView.ViewHolder(binding.root)
122 |
123 | inner class DailyUpdatesLoadingViewHolder(binding: ItemDailyLoadingBinding) :
124 | RecyclerView.ViewHolder(binding.root)
125 | }
--------------------------------------------------------------------------------
/app/src/main/res/raw/style_json.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "elementType": "geometry",
4 | "stylers": [
5 | {
6 | "color": "#1d2c4d"
7 | }
8 | ]
9 | },
10 | {
11 | "elementType": "labels.text.fill",
12 | "stylers": [
13 | {
14 | "color": "#8ec3b9"
15 | }
16 | ]
17 | },
18 | {
19 | "elementType": "labels.text.stroke",
20 | "stylers": [
21 | {
22 | "color": "#1a3646"
23 | }
24 | ]
25 | },
26 | {
27 | "featureType": "administrative.country",
28 | "elementType": "geometry.stroke",
29 | "stylers": [
30 | {
31 | "color": "#4b6878"
32 | }
33 | ]
34 | },
35 | {
36 | "featureType": "administrative.land_parcel",
37 | "elementType": "labels.text.fill",
38 | "stylers": [
39 | {
40 | "color": "#64779e"
41 | }
42 | ]
43 | },
44 | {
45 | "featureType": "administrative.province",
46 | "elementType": "geometry.stroke",
47 | "stylers": [
48 | {
49 | "color": "#4b6878"
50 | }
51 | ]
52 | },
53 | {
54 | "featureType": "landscape.man_made",
55 | "elementType": "geometry.stroke",
56 | "stylers": [
57 | {
58 | "color": "#334e87"
59 | }
60 | ]
61 | },
62 | {
63 | "featureType": "landscape.natural",
64 | "elementType": "geometry",
65 | "stylers": [
66 | {
67 | "color": "#023e58"
68 | }
69 | ]
70 | },
71 | {
72 | "featureType": "poi",
73 | "elementType": "geometry",
74 | "stylers": [
75 | {
76 | "color": "#283d6a"
77 | }
78 | ]
79 | },
80 | {
81 | "featureType": "poi",
82 | "elementType": "labels.text.fill",
83 | "stylers": [
84 | {
85 | "color": "#6f9ba5"
86 | }
87 | ]
88 | },
89 | {
90 | "featureType": "poi",
91 | "elementType": "labels.text.stroke",
92 | "stylers": [
93 | {
94 | "color": "#1d2c4d"
95 | }
96 | ]
97 | },
98 | {
99 | "featureType": "poi.park",
100 | "elementType": "geometry.fill",
101 | "stylers": [
102 | {
103 | "color": "#023e58"
104 | }
105 | ]
106 | },
107 | {
108 | "featureType": "poi.park",
109 | "elementType": "labels.text.fill",
110 | "stylers": [
111 | {
112 | "color": "#3C7680"
113 | }
114 | ]
115 | },
116 | {
117 | "featureType": "road",
118 | "elementType": "geometry",
119 | "stylers": [
120 | {
121 | "color": "#304a7d"
122 | }
123 | ]
124 | },
125 | {
126 | "featureType": "road",
127 | "elementType": "labels.text.fill",
128 | "stylers": [
129 | {
130 | "color": "#98a5be"
131 | }
132 | ]
133 | },
134 | {
135 | "featureType": "road",
136 | "elementType": "labels.text.stroke",
137 | "stylers": [
138 | {
139 | "color": "#1d2c4d"
140 | }
141 | ]
142 | },
143 | {
144 | "featureType": "road.highway",
145 | "elementType": "geometry",
146 | "stylers": [
147 | {
148 | "color": "#2c6675"
149 | }
150 | ]
151 | },
152 | {
153 | "featureType": "road.highway",
154 | "elementType": "geometry.stroke",
155 | "stylers": [
156 | {
157 | "color": "#255763"
158 | }
159 | ]
160 | },
161 | {
162 | "featureType": "road.highway",
163 | "elementType": "labels.text.fill",
164 | "stylers": [
165 | {
166 | "color": "#b0d5ce"
167 | }
168 | ]
169 | },
170 | {
171 | "featureType": "road.highway",
172 | "elementType": "labels.text.stroke",
173 | "stylers": [
174 | {
175 | "color": "#023e58"
176 | }
177 | ]
178 | },
179 | {
180 | "featureType": "transit",
181 | "elementType": "labels.text.fill",
182 | "stylers": [
183 | {
184 | "color": "#98a5be"
185 | }
186 | ]
187 | },
188 | {
189 | "featureType": "transit",
190 | "elementType": "labels.text.stroke",
191 | "stylers": [
192 | {
193 | "color": "#1d2c4d"
194 | }
195 | ]
196 | },
197 | {
198 | "featureType": "transit.line",
199 | "elementType": "geometry.fill",
200 | "stylers": [
201 | {
202 | "color": "#283d6a"
203 | }
204 | ]
205 | },
206 | {
207 | "featureType": "transit.station",
208 | "elementType": "geometry",
209 | "stylers": [
210 | {
211 | "color": "#3a4762"
212 | }
213 | ]
214 | },
215 | {
216 | "featureType": "water",
217 | "elementType": "geometry",
218 | "stylers": [
219 | {
220 | "color": "#0e1626"
221 | }
222 | ]
223 | },
224 | {
225 | "featureType": "water",
226 | "elementType": "labels.text.fill",
227 | "stylers": [
228 | {
229 | "color": "#4e6d70"
230 | }
231 | ]
232 | }
233 | ]
--------------------------------------------------------------------------------
/app/src/test/java/tech/awesome/coronatrack/ui/maps/MapsViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.maps
2 |
3 | import androidx.lifecycle.Observer
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import org.junit.After
6 | import org.junit.Before
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.mockito.BDDMockito.given
11 | import org.mockito.Mock
12 | import org.mockito.Mockito.*
13 | import org.mockito.MockitoAnnotations
14 | import org.mockito.junit.MockitoJUnitRunner
15 | import tech.awesome.coronatrack.CoroutineRule
16 | import tech.awesome.coronatrack.InstantRuleExecution
17 | import tech.awesome.coronatrack.runBlockingTest
18 | import tech.awesome.data.network.*
19 | import tech.awesome.domain.local.DBRepository
20 | import tech.awesome.domain.network.Repository
21 | import tech.awesome.utils.State
22 | import tech.awesome.utils.failure
23 | import tech.awesome.utils.value
24 |
25 |
26 | @ExperimentalCoroutinesApi
27 | @RunWith(MockitoJUnitRunner::class)
28 | class MapsViewModelTest {
29 |
30 | @get:Rule
31 | val coroutineRule = CoroutineRule()
32 |
33 | @Mock
34 | lateinit var repository: Repository
35 |
36 | @Mock
37 | lateinit var dbrepository: DBRepository
38 |
39 | private lateinit var viewModel: MapsViewModel
40 |
41 | @Mock
42 | private lateinit var observerConfirmed: Observer>>
43 |
44 | @Mock
45 | private lateinit var observerCountries: Observer>
46 |
47 |
48 | @Before
49 | fun setUp() {
50 | MockitoAnnotations.initMocks(this)
51 | InstantRuleExecution.start()
52 | viewModel = MapsViewModel(
53 | repository,
54 | dbrepository
55 | ).apply {
56 | confirmedState.observeForever(observerConfirmed)
57 | countryState.observeForever(observerCountries)
58 | }
59 | }
60 |
61 | @After
62 | fun tearDown() {
63 | InstantRuleExecution.tearDown()
64 | }
65 |
66 | @Test
67 | fun `when get confirmed data should success`() {
68 | coroutineRule.runBlockingTest {
69 | val data = emptyList()
70 | val success = value(data)
71 | given(repository.confirmed()).willReturn(success)
72 | viewModel.getConfirmed()
73 | verify(repository, atLeastOnce()).confirmed()
74 | verify(observerConfirmed, atLeastOnce()).onChanged(State.Loading)
75 | verify(observerConfirmed, atLeastOnce()).onChanged(State.Success(data))
76 | verifyNoMoreInteractions(repository, observerConfirmed)
77 | clearInvocations(repository, observerConfirmed)
78 | }
79 | }
80 |
81 | @Test
82 | fun `when get confirmed data should error`() {
83 | coroutineRule.runBlockingTest {
84 | val errorMessage = "No Data"
85 | val error = failure(Exception(errorMessage))
86 | given(repository.confirmed()).willReturn(error)
87 | viewModel.getConfirmed()
88 | verify(repository, atLeastOnce()).confirmed()
89 | verify(observerConfirmed, atLeastOnce()).onChanged(State.Loading)
90 | verify(observerConfirmed, atLeastOnce()).onChanged(State.Error(errorMessage))
91 | verifyNoMoreInteractions(repository, observerConfirmed)
92 | clearInvocations(repository, observerConfirmed)
93 | }
94 | }
95 |
96 | @Test
97 | fun `when get countries data should success`() {
98 | coroutineRule.runBlockingTest {
99 | val countries = emptyList()
100 | val data = Countries(countries)
101 | val success = value(data)
102 | given(repository.countries()).willReturn(success)
103 | viewModel.getCountries()
104 | verify(repository, atLeastOnce()).countries()
105 | verify(observerCountries, atLeastOnce()).onChanged(State.Loading)
106 | verify(observerCountries, atLeastOnce()).onChanged(State.Success(data))
107 | verifyNoMoreInteractions(repository, observerCountries)
108 | clearInvocations(repository, observerCountries)
109 | }
110 | }
111 |
112 | @Test
113 | fun `when get countries data should error`() {
114 | coroutineRule.runBlockingTest {
115 | val errorMessage = "No Data"
116 | val error = failure(Exception(errorMessage))
117 | given(repository.countries()).willReturn(error)
118 | viewModel.getCountries()
119 | verify(repository, atLeastOnce()).countries()
120 | verify(observerCountries, atLeastOnce()).onChanged(State.Loading)
121 | verify(observerCountries, atLeastOnce()).onChanged(State.Error(errorMessage))
122 | verifyNoMoreInteractions(repository, observerCountries)
123 | clearInvocations(repository, observerCountries)
124 | }
125 | }
126 |
127 | }
--------------------------------------------------------------------------------
/utils/src/main/java/tech/awesome/utils/Saver.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.utils
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import com.google.common.reflect.TypeParameter
6 | import com.google.common.reflect.TypeToken
7 | import com.google.gson.Gson
8 |
9 | class Saver {
10 |
11 | companion object {
12 | private var instance: Saver? = null
13 | private var mSharedPreferences: SharedPreferences? = null
14 |
15 | fun init(context: Context) {
16 | mSharedPreferences = context.getSharedPreferences(PrefKey.PREF_NAME, Context.MODE_PRIVATE)
17 | }
18 |
19 | fun instance(): Saver {
20 | if (instance == null) {
21 | validateInitialization()
22 | synchronized(Saver::class.java) {
23 | if (instance == null) {
24 | instance = Saver()
25 | }
26 | }
27 | }
28 | return instance!!
29 | }
30 |
31 | private fun validateInitialization() {
32 | if (mSharedPreferences == null)
33 | throw RuntimeException("SharePref Library must be initialized inside your application class by calling SharePref.init(getApplicationContext)")
34 | }
35 | }
36 |
37 | fun saveInt(key: String, value: Int) {
38 | val editor = mSharedPreferences!!.edit()
39 | editor.putInt(key, value)
40 | editor.apply()
41 | }
42 |
43 | fun getInt(key: String): Int {
44 | return if (isValidKey(key)) {
45 | mSharedPreferences!!.getInt(key, 0)
46 | } else 0
47 | }
48 |
49 | fun saveBoolean(key: String, value: Boolean) {
50 | val editor = mSharedPreferences!!.edit()
51 | editor.putBoolean(key, value)
52 | editor.apply()
53 | }
54 |
55 | fun getBoolean(key: String, default: Boolean): Boolean {
56 | return mSharedPreferences!!.getBoolean(key, default)
57 | }
58 |
59 |
60 | fun saveFloat(key: String, value: Float) {
61 | val editor = mSharedPreferences!!.edit()
62 | editor.putFloat(key, value)
63 | editor.apply()
64 | }
65 |
66 | fun getFloat(key: String): Float {
67 | return if (isValidKey(key)) {
68 | mSharedPreferences!!.getFloat(key, 0.0f)
69 | } else 0.0f
70 | }
71 |
72 |
73 | fun saveLong(key: String, value: Long) {
74 | val editor = mSharedPreferences!!.edit()
75 | editor.putLong(key, value)
76 | editor.apply()
77 | }
78 |
79 | fun getLong(key: String): Long {
80 | return if (isValidKey(key)) {
81 | mSharedPreferences!!.getLong(key, 0)
82 | } else 0
83 | }
84 |
85 |
86 | fun saveString(key: String, value: String) {
87 | val editor = mSharedPreferences!!.edit()
88 | editor.putString(key, value)
89 | editor.apply()
90 | }
91 |
92 | fun getString(key: String): String? {
93 | return if (isValidKey(key)) {
94 | mSharedPreferences!!.getString(key, null)
95 | } else null
96 | }
97 |
98 | fun saveObject(key: String, `object`: T) {
99 | val objectString = Gson().toJson(`object`)
100 | val editor = mSharedPreferences!!.edit()
101 | editor.putString(key, objectString)
102 | editor.apply()
103 | }
104 |
105 | fun getObject(key: String, classType: Class): T? {
106 | if (isValidKey(key)) {
107 | val objectString = mSharedPreferences!!.getString(key, null)
108 | if (objectString != null) {
109 | return Gson().fromJson(objectString, classType)
110 | }
111 | }
112 | return null
113 | }
114 |
115 |
116 | fun saveObjectsList(key: String, objectList: List) {
117 | val objectString = Gson().toJson(objectList)
118 | val editor = mSharedPreferences!!.edit()
119 | editor.putString(key, objectString)
120 | editor.apply()
121 | }
122 |
123 | fun getObjectsList(key: String, classType: Class): List? {
124 | if (isValidKey(key)) {
125 | val objectString = mSharedPreferences!!.getString(key, null)
126 | if (objectString != null) {
127 | return Gson().fromJson>(objectString, object : TypeToken>() {
128 |
129 | }
130 | .where(object : TypeParameter() {
131 |
132 | }, classType)
133 | .type
134 | )
135 | }
136 | }
137 |
138 | return null
139 | }
140 |
141 | fun clearSession() {
142 | val editor = mSharedPreferences!!.edit()
143 | editor.clear()
144 | editor.apply()
145 | }
146 |
147 | fun deleteValue(key: String): Boolean {
148 | if (isValidKey(key)) {
149 | val editor = mSharedPreferences!!.edit()
150 | editor.remove(key)
151 | editor.apply()
152 | return true
153 | }
154 |
155 | return false
156 | }
157 |
158 | private fun isValidKey(key: String): Boolean {
159 | val map = mSharedPreferences!!.all
160 | return map.containsKey(key)
161 | }
162 |
163 |
164 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_maps.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
14 |
15 |
20 |
21 |
25 |
26 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
56 |
57 |
58 |
59 |
60 |
61 |
71 |
72 |
83 |
84 |
107 |
108 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/awesome/coronatrack/ui/maps/MapsActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.awesome.coronatrack.ui.maps
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.text.Editable
7 | import android.text.TextWatcher
8 | import android.view.View
9 | import android.view.inputmethod.EditorInfo
10 | import androidx.constraintlayout.widget.ConstraintLayout
11 | import com.google.android.gms.maps.model.LatLng
12 | import com.google.android.material.bottomsheet.BottomSheetBehavior
13 | import org.koin.android.viewmodel.ext.android.viewModel
14 | import tech.awesome.coronatrack.R
15 | import tech.awesome.coronatrack.databinding.ActivityMapsBinding
16 | import tech.awesome.coronatrack.ui.base.BindingActivity
17 | import tech.awesome.coronatrack.ui.maps.fragment.MapFragment
18 | import tech.awesome.coronatrack.ui.maps.fragment.adapter.MapsAdapter
19 | import tech.awesome.data.network.Confirmed
20 | import tech.awesome.data.network.Countries
21 | import tech.awesome.utils.ExtraConstant
22 | import tech.awesome.utils.State
23 | import tech.awesome.utils.StatusConstant
24 | import tech.awesome.utils.extension.*
25 |
26 | class MapsActivity : BindingActivity(), MapsAdapter.MapsListener {
27 |
28 | companion object {
29 | fun getIntent(context: Context?): Intent =
30 | Intent(context, MapsActivity::class.java)
31 |
32 | fun getIntentWithFocus(context: Context?, latLng: LatLng) : Intent = getIntent(context).apply {
33 | putExtra(ExtraConstant.EXTRA_LATLNG, latLng)
34 | }
35 | }
36 |
37 | private val binding: ActivityMapsBinding by binding(R.layout.activity_maps)
38 | private val vm by viewModel()
39 | private val mAdapter by lazy { MapsAdapter(this, StatusConstant.CONFIRMED) }
40 | private var bottomSheetBehavior: BottomSheetBehavior? = null
41 | private lateinit var mapsFragment: MapFragment
42 |
43 | override fun onCreate(savedInstanceState: Bundle?) {
44 | super.onCreate(savedInstanceState)
45 | binding.rv.adapter = mAdapter
46 | binding.ivBack.setOnClickListener { onBackPressed() }
47 | initObserver()
48 | initViews()
49 | }
50 |
51 | private fun initObserver() {
52 | observe(vm.confirmedState, ::onUpdateConfirmedState)
53 | observe(vm.countryState, ::onUpdateCountriesState)
54 |
55 | }
56 |
57 | private fun onUpdateCountriesState(state: State) {
58 | when (state) {
59 | is State.Loading -> onLoadingRv()
60 | is State.Error -> {
61 | onLoadedRv()
62 | showToast(state.message)
63 | }
64 | is State.Success -> {
65 | mAdapter.setCountries(state.value)
66 | onLoadedRv()
67 | }
68 | }
69 |
70 | }
71 |
72 | private fun onUpdateConfirmedState(state: State>) {
73 | when (state) {
74 | is State.Loading -> onLoadingRv()
75 | is State.Error -> {
76 | onLoadedRv()
77 | showToast(state.message)
78 | }
79 | is State.Success -> {
80 | mAdapter.setData(state.value.toTypedArray())
81 | mapsFragment.setMarkers(state.value)
82 | binding.etSearch.text.clear()
83 |
84 | val latlng = intent.getParcelableExtra(ExtraConstant.EXTRA_LATLNG)
85 | if (latlng != null) {
86 | mapsFragment.setFocus(latlng)
87 | }
88 | onLoadedRv()
89 | }
90 | }
91 | }
92 |
93 |
94 | private fun onLoadingRv() {
95 | binding.rlPb.visible()
96 | }
97 |
98 | private fun onLoadedRv() {
99 | binding.rlPb.gone()
100 | }
101 |
102 | private fun initViews() {
103 | mapsFragment = MapFragment.newInstance(StatusConstant.CONFIRMED)
104 | mapsFragment.let {
105 | supportFragmentManager.beginTransaction().replace(binding.fMap.id, it)
106 | .commitAllowingStateLoss()
107 | }
108 |
109 | bottomSheetBehavior = BottomSheetBehavior.from(binding.bs)
110 | bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
111 | bottomSheetBehavior?.setBottomSheetCallback(object :
112 | BottomSheetBehavior.BottomSheetCallback() {
113 | override fun onSlide(p0: View, p1: Float) {}
114 |
115 | override fun onStateChanged(bottomSheet: View, newState: Int) {
116 | if (newState == BottomSheetBehavior.STATE_HIDDEN) {
117 | bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
118 | }
119 | }
120 | })
121 | with(binding.etSearch) {
122 | setOnEditorActionListener { _, actionId, _ ->
123 | if (actionId == EditorInfo.IME_ACTION_SEARCH) {
124 | hideKeyboard()
125 | }
126 | false
127 | }
128 |
129 | addTextChangedListener(object : TextWatcher {
130 | override fun afterTextChanged(mEditable: Editable?) {}
131 | override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
132 |
133 | override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
134 | mAdapter.setShownData(p0.toString())
135 | }
136 | })
137 | }
138 |
139 | vm.getCountries()
140 | vm.getConfirmed()
141 | }
142 |
143 |
144 | override fun onClickItem(confirmed: Confirmed, index: Int) {
145 | hideKeyboard()
146 | bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
147 | val latlng = LatLng(confirmed.lat ?: 0.0, confirmed.long ?: 0.0)
148 | mapsFragment.setFocus(latlng)
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------