├── .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 | 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 | 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 | ![Pic](https://user-images.githubusercontent.com/15356308/79304649-09e47380-7f1c-11ea-8e1b-a958a8cd079d.jpg) 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 | 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 | 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 | 6 | 7 | 8 | 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 | 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 | --------------------------------------------------------------------------------