Repository: PatilShreyas/Covid19-Notifier-IN Branch: master Commit: 3935d87a84af Files: 71 Total size: 150.7 KB Directory structure: gitextract_cjuovg5p/ ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── dependabot.yml │ └── workflows/ │ └── Build.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── dev/ │ │ └── shreyaspatil/ │ │ └── covid19notify/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── dev/ │ │ │ └── shreyaspatil/ │ │ │ └── covid19notify/ │ │ │ ├── CovidNotifyApp.kt │ │ │ ├── api/ │ │ │ │ └── Covid19IndiaApiService.kt │ │ │ ├── di/ │ │ │ │ ├── NetworkModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── model/ │ │ │ │ ├── Details.kt │ │ │ │ ├── StateDetails.kt │ │ │ │ └── StateResponse.kt │ │ │ ├── repository/ │ │ │ │ ├── CovidIndiaRepository.kt │ │ │ │ └── NetworkBoundRepository.kt │ │ │ ├── ui/ │ │ │ │ ├── adapter/ │ │ │ │ │ └── TotalAdapter.kt │ │ │ │ ├── details/ │ │ │ │ │ ├── StateDetailsActivity.kt │ │ │ │ │ ├── StateDetailsViewModel.kt │ │ │ │ │ └── adapter/ │ │ │ │ │ └── DistrictsAdapter.kt │ │ │ │ └── main/ │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ └── adapter/ │ │ │ │ └── ItemAdapter.kt │ │ │ ├── utils/ │ │ │ │ ├── NetworkUtils.kt │ │ │ │ ├── State.kt │ │ │ │ ├── ThemeUtils.kt │ │ │ │ └── TimeUtils.kt │ │ │ └── worker/ │ │ │ └── NotificationWorker.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_death.xml │ │ │ ├── ic_heart.xml │ │ │ ├── ic_info.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_lightbulb.xml │ │ │ ├── ic_patient.xml │ │ │ ├── ic_settings.xml │ │ │ ├── ic_trending_up.xml │ │ │ └── ic_virus.xml │ │ ├── drawable-anydpi-v24/ │ │ │ └── ic_stat_notification_icon.xml │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── activity_state_details.xml │ │ │ ├── appbar_layout.xml │ │ │ ├── item_district.xml │ │ │ ├── item_state.xml │ │ │ └── item_total.xml │ │ ├── menu/ │ │ │ └── toolbar_menu.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── bool.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ ├── styles.xml │ │ │ └── theme.xml │ │ └── values-night/ │ │ ├── bool.xml │ │ └── colors.xml │ └── test/ │ └── java/ │ └── dev/ │ └── shreyaspatil/ │ └── covid19notify/ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ * @PatilShreyas ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: PatilShreyas otechie: # Replace with a single Otechie username custom: ['https://www.paypal.me/PatilShreyas99/'] ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "gradle" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/Build.yml ================================================ name: Build on: [push, pull_request] jobs: test: name: Run Unit Tests runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v1 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Grant Permission to Execute run: chmod +x gradlew - name: Unit tests run: bash ./gradlew test --stacktrace apk: name: Generate APK runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v1 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Grant Permission to Execute run: chmod +x gradlew - name: Build debug APK run: bash ./gradlew assembleDebug --stacktrace - name: Upload APK uses: actions/upload-artifact@v1 with: name: app path: app/build/outputs/apk/debug/app-debug.apk ================================================ FILE: .gitignore ================================================ #built application files *.apk *.ap_ # files for the dex VM *.dex # Java class files *.class # generated files bin/ gen/ # Local configuration file (sdk path, etc) local.properties # Windows thumbnail db Thumbs.db # OSX files .DS_Store # Android Studio *.iml .idea #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. .gradle build/ .navigation captures/ output.json #NDK obj/ .externalNativeBuild ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at shreyaspatilg@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ ## Feeling Awesome! Thanks for thinking about this. You can contribute us by filing issues, bugs and PRs. ### Contributing: - Open issue regarding proposed change. - Repo owner will contact you there. - If your proposed change is approved, Fork this repo and do changes. - Open PR against latest `dev` branch. Add nice description in PR. - You're done! ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Shreyas Patil Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

# COVID19 Notifier - India 🇮🇳 ![CI](https://github.com/PatilShreyas/Covid19-Notifier-IN/workflows/CI/badge.svg?branch=master) [![GitHub license](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) ![GitHub downloads](https://img.shields.io/github/downloads/PatilShreyas/Covid19-Notifier-IN/total?color=blue&label=Downloads&logo=android&style=flat-square) ![Github Followers](https://img.shields.io/github/followers/PatilShreyas?label=Follow&style=social) ![GitHub stars](https://img.shields.io/github/stars/PatilShreyas/Covid19-Notifier-IN?style=social) ![GitHub forks](https://img.shields.io/github/forks/PatilShreyas/Covid19-Notifier-IN?style=social) ![Twitter Follow](https://img.shields.io/twitter/follow/imShreyasPatil?label=Follow&style=social) **Covid19 Notifier India** is a sample Android application 📱 built to demonstrate use of *Modern Android development* tools. Dedicated to all Android Developers with ❤️. ***You can Install and test latest Covid19 Notifier app from below 👇*** [![Covid19 App](https://img.shields.io/github/v/release/patilshreyas/covid19-notifier-in?color=%23FFFF&label=Download%20APK&logo=android)](https://github.com/patilshreyas/covid19-notifier-in/releases/latest/download/app-debug.apk)
Main Screen (Total Report) Main Screen (State Report List) Main Screen (Dark Mode 🌗) Notification in the System Tray
## About - It simply loads **Total COVID19 cases in India** from [API](https://github.com/covid19india/api). - It notifies total cases of COVID19 in India after every 1 hours. - It shows the total cases of the District from every State. - Dark mode too 🌗. - It is offline capable (Using Cache) 😃. *It uses `PeriodicWorkManager` which is scheduled at the first run of an app. After that, `Worker` will execute after every one hour of interval and will show notification on Android's system tray.* ## Built With 🛠 - [Kotlin](https://kotlinlang.org/) - First class and official programming language for Android development. - [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - For asynchronous and more.. - [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/) - A cold asynchronous data stream that sequentially emits values and completes normally or with an exception. - [Android Architecture Components](https://developer.android.com/topic/libraries/architecture) - Collection of libraries that help you design robust, testable, and maintainable apps. - [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) - Data objects that notify views when the underlying database changes. - [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Stores UI-related data that isn't destroyed on UI changes. - [ViewBinding](https://developer.android.com/topic/libraries/view-binding) - Generates a binding class for each XML layout file present in that module and allows you to more easily write code that interacts with views. - [Koin](https://start.insert-koin.io/) - Dependency Injection Framework (Kotlin) - [Retrofit](https://square.github.io/retrofit/) - A type-safe HTTP client for Android and Java. - [Moshi](https://github.com/square/moshi) - A modern JSON library for Kotlin and Java. - [Moshi Converter](https://github.com/square/retrofit/tree/master/retrofit-converters/moshi) - A Converter which uses Moshi for serialization to and from JSON. - [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager) - The WorkManager API makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts. - [Material Components for Android](https://github.com/material-components/material-components-android) - Modular and customizable Material Design UI components for Android # Package Structure dev.shreyaspatil.covid19notify # Root Package . ├── api # For API Service. ├── model # Model classes ├── repository # Repository to handle data from network using API. ├── di # Dependency Injection | ├── ui # Activity/View layer │ └── main # Main Screen Activity, ViewModel and RecyclerView Adapters. | ├── utils # Utility Classes / Kotlin extensions └── worker # Worker class. ## Contribute If you want to contribute to this project, you're always welcome! See [Contributing Guidelines](CONTRIBUTING.md). ## Credits Thanks to [COVID19India.org](https://github.com/covid19india/api) for open-source API. ## Contact If you need any help, you can connect with me. Visit:- [shreyaspatil.dev](https://shreyaspatil.dev) ## Contributed By: - [Shreyas Patil](https://shreyaspatil.dev) (Maintainer) - [Rohan Singh](https://twitter.com/zaraki596) (Contributer) ## License ``` MIT License Copyright (c) 2020 Shreyas Patil Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 30 buildToolsVersion "30.0.0" defaultConfig { applicationId "dev.shreyaspatil.covid19notify" minSdkVersion 21 targetSdkVersion 30 versionCode 4 versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } viewBinding { enabled = true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = "1.8" } } dependencies { def coroutines_version = '1.3.8' def appcompat_version = '1.2.0' def preference_version = '1.1.1' def core_ktx_version = '1.3.2' def constraintlayout_version = '2.0.4' def swiperefreshlayout_version = '1.1.0' def recyclerview_version = "1.2.0-alpha03" def lifecycle_version = '2.2.0' def material_design_version = '1.2.1' def retrofit_version = '2.9.0' def moshi_version = '1.11.0' def workmanager_version = '2.4.0' def koin_version = '2.1.6' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" // Android implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.core:core-ktx:$core_ktx_version" implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" implementation "androidx.swiperefreshlayout:swiperefreshlayout:$swiperefreshlayout_version" implementation "androidx.recyclerview:recyclerview:$recyclerview_version" implementation "androidx.preference:preference:$preference_version" // Architecture Components implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Material Design implementation "com.google.android.material:material:$material_design_version" // Retrofit implementation "com.squareup.retrofit2:retrofit:$retrofit_version" // Moshi implementation "com.squareup.moshi:moshi-kotlin:$moshi_version" implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" // WorkManager implementation "androidx.work:work-runtime-ktx:$workmanager_version" // Koin DI implementation "org.koin:koin-core:$koin_version" implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android-scope:$koin_version" implementation "org.koin:koin-android-viewmodel:$koin_version" // Testing testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/androidTest/java/dev/shreyaspatil/covid19notify/ExampleInstrumentedTest.kt ================================================ package dev.shreyaspatil.covid19notify import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("dev.shreyaspatil.covid19notify", appContext.packageName) } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/CovidNotifyApp.kt ================================================ package dev.shreyaspatil.covid19notify import android.app.Application import dev.shreyaspatil.covid19notify.di.networkModule import dev.shreyaspatil.covid19notify.di.viewModelModule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.InternalCoroutinesApi import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @FlowPreview @ExperimentalCoroutinesApi @InternalCoroutinesApi class CovidNotifyApp : Application() { override fun onCreate() { super.onCreate() initKoin() } private fun initKoin() { startKoin { androidContext(applicationContext) modules(networkModule, viewModelModule) } } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/api/Covid19IndiaApiService.kt ================================================ package dev.shreyaspatil.covid19notify.api import dev.shreyaspatil.covid19notify.model.StateDetailsResponse import dev.shreyaspatil.covid19notify.model.StateResponse import retrofit2.Response import retrofit2.http.GET interface Covid19IndiaApiService { @GET("data.json") suspend fun getData(): Response @GET("v2/state_district_wise.json") suspend fun getStateDistrictData(): Response> companion object { const val BASE_URL = "https://api.covid19india.org/" } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/di/NetworkModule.kt ================================================ package dev.shreyaspatil.covid19notify.di import android.content.Context import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dev.shreyaspatil.covid19notify.api.Covid19IndiaApiService import dev.shreyaspatil.covid19notify.repository.CovidIndiaRepository import dev.shreyaspatil.covid19notify.utils.isNetworkAvailable import kotlinx.coroutines.ExperimentalCoroutinesApi import okhttp3.Cache import okhttp3.OkHttpClient import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory @ExperimentalCoroutinesApi val networkModule = module { single { Retrofit.Builder() .baseUrl(Covid19IndiaApiService.BASE_URL) .addConverterFactory( MoshiConverterFactory.create( Moshi.Builder().add(KotlinJsonAdapterFactory()).build() ) ) .client(getOkHttpClient(androidContext())) .build() .create(Covid19IndiaApiService::class.java) } single { CovidIndiaRepository(get()) } } fun getOkHttpClient(context: Context): OkHttpClient { val cacheSize = (5 * 1024 * 1024).toLong() val myCache = Cache(context.cacheDir, cacheSize) return OkHttpClient.Builder() .cache(myCache) .addInterceptor { chain -> var request = chain.request() request = if (isNetworkAvailable(context)!!) request.newBuilder().header("Cache-Control", "public, max-age=" + 5).build() else request.newBuilder().header( "Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7 ).build() chain.proceed(request) } .build() } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/di/ViewModelModule.kt ================================================ package dev.shreyaspatil.covid19notify.di import dev.shreyaspatil.covid19notify.ui.details.StateDetailsViewModel import dev.shreyaspatil.covid19notify.ui.main.MainViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.InternalCoroutinesApi import org.koin.android.viewmodel.dsl.viewModel import org.koin.dsl.module @FlowPreview @ExperimentalCoroutinesApi @InternalCoroutinesApi val viewModelModule = module { viewModel { MainViewModel(get()) } viewModel { StateDetailsViewModel(get()) } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/model/Details.kt ================================================ package dev.shreyaspatil.covid19notify.model import android.os.Parcelable import com.squareup.moshi.Json import kotlinx.android.parcel.Parcelize @Parcelize data class Details( val active: String = "0", val confirmed: String = "0", val recovered: String = "0", val deaths: String = "0", val state: String = "", @Json(name = "statenotes") val stateNotes: String = "", @Json(name = "deltaconfirmed") val deltaConfirmed: String = "0", @Json(name = "deltarecovered") val deltaRecovered: String = "0", @Json(name = "deltadeaths") val deltaDeaths: String = "0", @Json(name = "lastupdatedtime") val lastUpdatedTime: String = "" ) : Parcelable ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/model/StateDetails.kt ================================================ package dev.shreyaspatil.covid19notify.model import android.os.Parcelable import com.squareup.moshi.JsonClass import kotlinx.android.parcel.Parcelize @Parcelize @JsonClass(generateAdapter = true) data class StateDetailsResponse( val districtData: List, val state: String ) : Parcelable @Parcelize @JsonClass(generateAdapter = true) data class DistrictData( val confirmed: Int = 0, val active: Int = 0, val deceased: Int = 0, val recovered: Int = 0, val delta: Delta, val notes: String = "", val district: String = "" ) : Parcelable @Parcelize @JsonClass(generateAdapter = true) data class Delta( val confirmed: Int = 0, val active: Int = 0, val deceased: Int = 0, val recovered: Int = 0 ) : Parcelable ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/model/StateResponse.kt ================================================ package dev.shreyaspatil.covid19notify.model import android.os.Parcelable import com.squareup.moshi.Json import kotlinx.android.parcel.Parcelize @Parcelize data class StateResponse( @Json(name = "statewise") val stateWiseDetails: List
) : Parcelable ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/repository/CovidIndiaRepository.kt ================================================ package dev.shreyaspatil.covid19notify.repository import dev.shreyaspatil.covid19notify.api.Covid19IndiaApiService import dev.shreyaspatil.covid19notify.model.StateDetailsResponse import dev.shreyaspatil.covid19notify.model.StateResponse import dev.shreyaspatil.covid19notify.utils.State import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import retrofit2.Response @FlowPreview @ExperimentalCoroutinesApi class CovidIndiaRepository(private val apiService: Covid19IndiaApiService) { fun getData(): Flow> { return object : NetworkBoundRepository() { override suspend fun fetchFromRemote(): Response = apiService.getData() }.asFlow().flowOn(Dispatchers.IO) } fun getStateDetailsData(stateName: String): Flow> { return object : NetworkBoundRepository>() { override suspend fun fetchFromRemote(): Response> = apiService.getStateDistrictData() }.asFlow().flowOn(Dispatchers.IO).map { state -> when (state) { is State.Loading -> State.loading() is State.Success -> { val data = state.data.find { it.state == stateName } if (data != null) { State.success(data) } else { State.error("No data found of state '$stateName'") } } is State.Error -> { State.error(state.message) } } } } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/repository/NetworkBoundRepository.kt ================================================ package dev.shreyaspatil.covid19notify.repository import androidx.annotation.MainThread import dev.shreyaspatil.covid19notify.utils.State import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow import retrofit2.Response @ExperimentalCoroutinesApi abstract class NetworkBoundRepository { fun asFlow() = flow> { // Emit Loading State emit(State.loading()) try { // Fetch latest data from remote val apiResponse = fetchFromRemote() // Parse body val remotePosts = apiResponse.body() // Check for response validation if (apiResponse.isSuccessful && remotePosts != null) { emit(State.success(remotePosts)) } else { // Something went wrong! Emit Error state. emit(State.error(apiResponse.message())) } } catch (e: Exception) { // Exception occurred! Emit error emit(State.error("Network error! Can't get latest data.")) e.printStackTrace() } } @MainThread protected abstract suspend fun fetchFromRemote(): Response } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/ui/adapter/TotalAdapter.kt ================================================ package dev.shreyaspatil.covid19notify.ui.adapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import dev.shreyaspatil.covid19notify.databinding.ItemTotalBinding import dev.shreyaspatil.covid19notify.model.Details class TotalAdapter : ListAdapter(DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TotalViewHolder( ItemTotalBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) override fun onBindViewHolder(holder: TotalViewHolder, position: Int) = holder.bind(getItem(position)) class TotalViewHolder(private val binding: ItemTotalBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(details: Details) { binding.textConfirmed.text = details.confirmed binding.textActive.text = details.active binding.textRecovered.text = details.recovered binding.textDeceased.text = details.deaths // New Confirmed details.deltaConfirmed.let { if (it == "0") { binding.groupNewConfirmed.visibility = View.GONE } else { binding.groupNewConfirmed.visibility = View.VISIBLE binding.textNewConfirmed.text = details.deltaConfirmed } } // New Recovered details.deltaRecovered.let { if (it == "0") { binding.groupNewRecovered.visibility = View.GONE } else { binding.groupNewRecovered.visibility = View.VISIBLE binding.textNewRecovered.text = details.deltaRecovered } } // New Deaths details.deltaDeaths.let { if (it == "0") { binding.groupNewDeaths.visibility = View.GONE } else { binding.groupNewDeaths.visibility = View.VISIBLE binding.textNewDeaths.text = details.deltaDeaths } } } } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback
() { override fun areItemsTheSame(oldItem: Details, newItem: Details): Boolean = oldItem.state == newItem.state override fun areContentsTheSame(oldItem: Details, newItem: Details): Boolean = oldItem == newItem } } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/ui/details/StateDetailsActivity.kt ================================================ package dev.shreyaspatil.covid19notify.ui.details import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.recyclerview.widget.MergeAdapter import dev.shreyaspatil.covid19notify.R import dev.shreyaspatil.covid19notify.databinding.ActivityStateDetailsBinding import dev.shreyaspatil.covid19notify.model.Details import dev.shreyaspatil.covid19notify.ui.adapter.TotalAdapter import dev.shreyaspatil.covid19notify.ui.details.adapter.DistrictsAdapter import dev.shreyaspatil.covid19notify.utils.State import dev.shreyaspatil.covid19notify.utils.getPeriod import dev.shreyaspatil.covid19notify.utils.toDateFormat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.InternalCoroutinesApi import org.koin.android.viewmodel.ext.android.viewModel @FlowPreview @ExperimentalCoroutinesApi @InternalCoroutinesApi class StateDetailsActivity : AppCompatActivity() { private lateinit var binding: ActivityStateDetailsBinding private val mStateTotalAdapter = TotalAdapter() private val mDistrictAdapter = DistrictsAdapter() private val adapter = MergeAdapter(mStateTotalAdapter, mDistrictAdapter) private val viewModel: StateDetailsViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityStateDetailsBinding.inflate(layoutInflater) setContentView(binding.root) initViews() initData() } private fun getStateDetails(): Details? = intent.getParcelableExtra(KEY_STATE_DETAILS) private fun initViews() { setSupportActionBar(binding.appBarlayout.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.recyclerState.adapter = adapter val details: Details? = getStateDetails() details?.let { mStateTotalAdapter.submitList(listOf(it)) supportActionBar?.title = it.state supportActionBar?.subtitle = getString( R.string.text_last_updated, getPeriod( it.lastUpdatedTime.toDateFormat() ) ) } binding.swipeRefreshLayout.setOnRefreshListener { loadData(getStateDetails()) } } private fun initData() { viewModel.stateCovidLiveDataDetails.observe(this, Observer { state -> when (state) { is State.Loading -> { binding.swipeRefreshLayout.isRefreshing = true } is State.Success -> { val list = state.data.districtData list.sortedByDescending { it.confirmed }.let { districtList -> mDistrictAdapter.submitList(districtList) } binding.swipeRefreshLayout.isRefreshing = false } is State.Error -> { binding.swipeRefreshLayout.isRefreshing = false Toast.makeText(applicationContext, state.message, Toast.LENGTH_LONG).show() } } }) if (viewModel.stateCovidLiveDataDetails.value !is State.Success) { loadData(getStateDetails()) } } private fun loadData(details: Details?) { details?.state?.let { viewModel.getDistrictData(it) } } companion object { const val KEY_STATE_DETAILS = "key_state_details" } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/ui/details/StateDetailsViewModel.kt ================================================ package dev.shreyaspatil.covid19notify.ui.details import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dev.shreyaspatil.covid19notify.model.StateDetailsResponse import dev.shreyaspatil.covid19notify.repository.CovidIndiaRepository import dev.shreyaspatil.covid19notify.utils.State import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @FlowPreview @ExperimentalCoroutinesApi @InternalCoroutinesApi class StateDetailsViewModel(private val covidIndiaRepository: CovidIndiaRepository) : ViewModel() { private val _stateCovidLiveData = MutableLiveData>() val stateCovidLiveDataDetails: LiveData> = _stateCovidLiveData fun getDistrictData(state: String) { viewModelScope.launch { covidIndiaRepository.getStateDetailsData(state).collect { _stateCovidLiveData.value = it } } } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/ui/details/adapter/DistrictsAdapter.kt ================================================ package dev.shreyaspatil.covid19notify.ui.details.adapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import dev.shreyaspatil.covid19notify.databinding.ItemDistrictBinding import dev.shreyaspatil.covid19notify.model.DistrictData class DistrictsAdapter : ListAdapter(DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TotalViewHolder( ItemDistrictBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) override fun onBindViewHolder(holder: TotalViewHolder, position: Int) = holder.bind(getItem(position)) class TotalViewHolder(private val binding: ItemDistrictBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(details: DistrictData) { binding.textDistrictName.text = details.district binding.textConfirmed.text = details.confirmed.toString() binding.textActive.text = details.active.toString() binding.textDeath.text = details.deceased.toString() binding.textRecovered.text = details.recovered.toString() // New Confirmed details.delta.confirmed.let { if (it == 0) { binding.groupStateNewConfirm.visibility = View.GONE } else { binding.groupStateNewConfirm.visibility = View.VISIBLE binding.textDistrictNewConfirm.text = details.delta.confirmed.toString() } } // New Recovered details.delta.recovered.let { if (it == 0) { binding.groupStateNewRecover.visibility = View.GONE } else { binding.groupStateNewRecover.visibility = View.VISIBLE binding.textDistrictNewRecover.text = details.delta.recovered.toString() } } // New Deaths details.delta.deceased.let { if (it == 0) { binding.groupStateNewDeaths.visibility = View.GONE } else { binding.groupStateNewDeaths.visibility = View.VISIBLE binding.textDistrictNewDeath.text = details.delta.deceased.toString() } } // Handling cases with notes details.notes.let { if (it.isBlank()) { binding.textNotes.visibility = View.GONE } else { binding.textNotes.visibility = View.VISIBLE binding.textNotes.text = details.notes } } } } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DistrictData, newItem: DistrictData): Boolean = oldItem.district == newItem.district override fun areContentsTheSame(oldItem: DistrictData, newItem: DistrictData): Boolean = oldItem == newItem } } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/ui/main/MainActivity.kt ================================================ package dev.shreyaspatil.covid19notify.ui.main import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.lifecycle.Observer import androidx.recyclerview.widget.MergeAdapter import androidx.work.* import com.google.android.material.snackbar.Snackbar import dev.shreyaspatil.covid19notify.R import dev.shreyaspatil.covid19notify.databinding.ActivityMainBinding import dev.shreyaspatil.covid19notify.model.Details import dev.shreyaspatil.covid19notify.ui.adapter.TotalAdapter import dev.shreyaspatil.covid19notify.ui.details.StateDetailsActivity import dev.shreyaspatil.covid19notify.ui.main.adapter.ItemAdapter import dev.shreyaspatil.covid19notify.utils.* import dev.shreyaspatil.covid19notify.worker.NotificationWorker import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.InternalCoroutinesApi import org.koin.android.viewmodel.ext.android.viewModel import java.util.concurrent.TimeUnit @FlowPreview @ExperimentalCoroutinesApi @InternalCoroutinesApi class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val viewModel: MainViewModel by viewModel() private val mTotalAdapter = TotalAdapter() private val mStateAdapter = ItemAdapter(this::navigateToStateDetailsActivity) private val adapter = MergeAdapter(mTotalAdapter, mStateAdapter) // Useful when back navigation is pressed. private var backPressedTime = 0L private val backSnackbar by lazy { Snackbar.make(binding.root, BACK_PRESSED_MESSAGE, Snackbar.LENGTH_SHORT) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) initViews() initData() initWorker() } private fun initViews() { setSupportActionBar(binding.appBarlayout.toolbar) binding.recycler.adapter = adapter binding.swipeRefreshLayout.setOnRefreshListener { loadData() } } override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.toolbar_menu, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_uimode -> { val uiMode = if (isDarkTheme()) { AppCompatDelegate.MODE_NIGHT_NO } else { AppCompatDelegate.MODE_NIGHT_YES } applyTheme(uiMode) true } else -> super.onOptionsItemSelected(item) } } private fun initData() { viewModel.covidLiveData.observe(this, Observer { state -> when (state) { is State.Loading -> binding.swipeRefreshLayout.isRefreshing = true is State.Error -> { binding.swipeRefreshLayout.isRefreshing = false Toast.makeText(applicationContext, state.message, Toast.LENGTH_LONG).show() } is State.Success -> { binding.swipeRefreshLayout.isRefreshing = false val list = state.data.stateWiseDetails mTotalAdapter.submitList(list.subList(0, 1)) mStateAdapter.submitList(list.subList(1, list.size - 1)) // Set Last Updated Time supportActionBar?.subtitle = getString( R.string.text_last_updated, getPeriod( list[0].lastUpdatedTime.toDateFormat() ) ) } } }) if (viewModel.covidLiveData.value !is State.Success) { loadData() } } private fun loadData() { viewModel.getData() } private fun navigateToStateDetailsActivity(details: Details) { startActivity(Intent(this, StateDetailsActivity::class.java).apply { putExtra(StateDetailsActivity.KEY_STATE_DETAILS, details) }) } private fun initWorker() { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val notificationWorkRequest = PeriodicWorkRequestBuilder(1, TimeUnit.HOURS) .setConstraints(constraints) .build() WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork( JOB_TAG, ExistingPeriodicWorkPolicy.KEEP, notificationWorkRequest ) } override fun onBackPressed() { if (backPressedTime + 2000 > System.currentTimeMillis()) { backSnackbar.dismiss() super.onBackPressed() return } else { backSnackbar.show() } backPressedTime = System.currentTimeMillis() } companion object { const val JOB_TAG = "notificationWorkTag" const val BACK_PRESSED_MESSAGE = "Press back again to exit" } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/ui/main/MainViewModel.kt ================================================ package dev.shreyaspatil.covid19notify.ui.main import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dev.shreyaspatil.covid19notify.model.StateResponse import dev.shreyaspatil.covid19notify.repository.CovidIndiaRepository import dev.shreyaspatil.covid19notify.utils.State import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @FlowPreview @ExperimentalCoroutinesApi @InternalCoroutinesApi class MainViewModel(private val repository: CovidIndiaRepository) : ViewModel() { private val _covidLiveData = MutableLiveData>() val covidLiveData: LiveData> = _covidLiveData fun getData() { viewModelScope.launch { repository.getData().collect { _covidLiveData.value = it } } } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/ui/main/adapter/ItemAdapter.kt ================================================ package dev.shreyaspatil.covid19notify.ui.main.adapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import dev.shreyaspatil.covid19notify.R import dev.shreyaspatil.covid19notify.databinding.ItemStateBinding import dev.shreyaspatil.covid19notify.model.Details import dev.shreyaspatil.covid19notify.utils.getPeriod import dev.shreyaspatil.covid19notify.utils.toDateFormat class ItemAdapter(val clickListener: (stateDetails: Details) -> Unit = {}) : ListAdapter(DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = StateViewHolder( ItemStateBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) override fun onBindViewHolder(holder: StateViewHolder, position: Int) = holder.bind(getItem(position)) inner class StateViewHolder(private val binding: ItemStateBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(details: Details) { binding.textState.text = details.state binding.textLastUpdatedView.text = itemView.context.getString( R.string.text_last_updated, getPeriod( details.lastUpdatedTime.toDateFormat() ) ) binding.textConfirmed.text = details.confirmed binding.textActive.text = details.active binding.textRecovered.text = details.recovered binding.textDeath.text = details.deaths // New Confirmed details.deltaConfirmed.let { if (it == "0") { binding.groupStateNewConfirm.visibility = View.GONE } else { binding.groupStateNewConfirm.visibility = View.VISIBLE binding.textStateNewConfirm.text = it } } // New Recovered details.deltaRecovered.let { if (it == "0") { binding.groupStateNewRecover.visibility = View.GONE } else { binding.groupStateNewRecover.visibility = View.VISIBLE binding.textStateNewRecover.text = it } } // New Deaths details.deltaDeaths.let { if (it == "0") { binding.groupStateNewDeaths.visibility = View.GONE } else { binding.groupStateNewDeaths.visibility = View.VISIBLE binding.textStateNewDeath.text = it } } // Set Click Listener binding.root.setOnClickListener { if (bindingAdapterPosition == RecyclerView.NO_POSITION) { return@setOnClickListener } val item = getItem(bindingAdapterPosition) item.let { clickListener.invoke(it) } } } } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback
() { override fun areItemsTheSame(oldItem: Details, newItem: Details): Boolean = oldItem.state == newItem.state override fun areContentsTheSame(oldItem: Details, newItem: Details): Boolean = oldItem == newItem } } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/utils/NetworkUtils.kt ================================================ package dev.shreyaspatil.covid19notify.utils import android.content.Context import android.net.ConnectivityManager import android.net.NetworkInfo fun isNetworkAvailable(context: Context): Boolean? { var isConnected: Boolean? = false // Initial Value val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val activeNetwork: NetworkInfo? = connectivityManager.activeNetworkInfo if (activeNetwork != null && activeNetwork.isConnected) isConnected = true return isConnected } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/utils/State.kt ================================================ package dev.shreyaspatil.covid19notify.utils sealed class State { class Loading : State() data class Success(val data: T) : State() data class Error(val message: String) : State() companion object { fun loading() = Loading() fun success(data: T) = Success(data) fun error(message: String) = Error(message) } } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/utils/ThemeUtils.kt ================================================ package dev.shreyaspatil.covid19notify.utils import android.content.res.Configuration import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import java.util.* fun applyTheme(theme: Int) { AppCompatDelegate.setDefaultNightMode(theme) } /** * Returns if currently dark theme is active or not. */ fun AppCompatActivity.isDarkTheme(): Boolean { return (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) } /** * Returns [Boolean] based on current time. * Returns true if hours are between 06:00 pm - 07:00 am */ fun isNight(): Boolean { val currentHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) return (currentHour <= 7 || currentHour >= 18) } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/utils/TimeUtils.kt ================================================ package dev.shreyaspatil.covid19notify.utils import android.annotation.SuppressLint import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.TimeUnit /** * Represents past time in text. * For e.g. 1 Minutes ago, 1 hour 0 minutes ago. */ @SuppressLint("SimpleDateFormat") fun getPeriod(past: Date): String { val now = Date() val seconds = TimeUnit.MILLISECONDS.toSeconds(now.time - past.time) val minutes = TimeUnit.MILLISECONDS.toMinutes(now.time - past.time) val hours = TimeUnit.MILLISECONDS.toHours(now.time - past.time) return when { seconds < 60 -> { "Few seconds ago" } minutes < 60 -> { "$minutes minutes ago" } hours < 24 -> { "$hours hour ${minutes % 60} min ago" } else -> { SimpleDateFormat("dd/MM/yy, hh:mm a").format(past).toString() } } } /** * Parses String to "dd/MM/yyyy HH:mm:ss" date and time format. */ @SuppressLint("SimpleDateFormat") fun String.toDateFormat(): Date { return SimpleDateFormat("dd/MM/yyyy HH:mm:ss") .parse(this) } ================================================ FILE: app/src/main/java/dev/shreyaspatil/covid19notify/worker/NotificationWorker.kt ================================================ package dev.shreyaspatil.covid19notify.worker import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.media.RingtoneManager import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import dev.shreyaspatil.covid19notify.R import dev.shreyaspatil.covid19notify.model.StateResponse import dev.shreyaspatil.covid19notify.repository.CovidIndiaRepository import dev.shreyaspatil.covid19notify.ui.main.MainActivity import dev.shreyaspatil.covid19notify.utils.State import dev.shreyaspatil.covid19notify.utils.getPeriod import dev.shreyaspatil.covid19notify.utils.toDateFormat import kotlinx.coroutines.* import kotlinx.coroutines.flow.toList import org.koin.core.KoinComponent import org.koin.core.get @FlowPreview @ExperimentalCoroutinesApi @InternalCoroutinesApi class NotificationWorker( private val context: Context, params: WorkerParameters ) : CoroutineWorker(context, params), KoinComponent { @SuppressLint("StringFormatInvalid") private fun showNotification(totalCount: String, time: String) { val intent = Intent(context, MainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) val pendingIntent = PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_ONE_SHOT ) val channelId = context.getString(R.string.default_notification_channel_id) val channelName = context.getString(R.string.default_notification_channel_name) val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val notificationBuilder = NotificationCompat.Builder(context, channelId) .setColor(ContextCompat.getColor(context, R.color.color_confirmed)) .setSmallIcon(R.drawable.ic_stat_notification_icon) .setContentTitle(context.getString(R.string.text_confirmed_cases, totalCount)) .setContentText(context.getString(R.string.text_last_updated, time)) .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_HIGH) .setSound(defaultSoundUri) .setContentIntent(pendingIntent) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Since android Oreo notification channel is needed. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( channelId, channelName, NotificationManager.IMPORTANCE_HIGH ) notificationManager.createNotificationChannel(channel) } notificationManager.notify(0, notificationBuilder.build()) } override suspend fun doWork(): Result = coroutineScope { Log.d(javaClass.simpleName, "Worker Started!") val repository: CovidIndiaRepository = get() val result = withContext(Dispatchers.Default) { repository.getData().toList() }.filterIsInstance>() if (result.isNullOrEmpty()) { Log.d(javaClass.simpleName, "Work Failed. Retrying...") Result.retry() } else { val totalDetails = result[0].data.stateWiseDetails[0] showNotification( totalDetails.confirmed, getPeriod( totalDetails.lastUpdatedTime.toDateFormat() ) ) Log.d(javaClass.simpleName, "Notification Displayed!") Log.d(javaClass.simpleName, "Work Succeed...") Result.success() } } } ================================================ FILE: app/src/main/res/drawable/ic_death.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_heart.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_info.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_lightbulb.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_patient.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_settings.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_trending_up.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_virus.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi-v24/ic_stat_notification_icon.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_state_details.xml ================================================ ================================================ FILE: app/src/main/res/layout/appbar_layout.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_district.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_state.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_total.xml ================================================ ================================================ FILE: app/src/main/res/menu/toolbar_menu.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/bool.xml ================================================ true ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #FFFFFF #0E0E0E #000 #F1F1F1 #D32F2F #388E3C #1976D2 #6D6D6D #FFFFFF #000 #000 #000 #000 ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 16dp 4dp 8dp 24dp 72dp ================================================ FILE: app/src/main/res/values/ic_launcher_background.xml ================================================ #FFFFFF ================================================ FILE: app/src/main/res/values/strings.xml ================================================ COVID19 Notifier 🇮🇳 Confirmed Active Recovered Deaths default Confirmed Cases : %s Last Updated : %s Covid-19 India Dark Mode ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/main/res/values/theme.xml ================================================ ================================================ FILE: app/src/main/res/values-night/bool.xml ================================================ false ================================================ FILE: app/src/main/res/values-night/colors.xml ================================================ #0E0E0E #000000 #F1FFAA00 #000000 #121212 #EF6C6C #6BC697 #65D1D6 #A0A0A0 #F1E5BC #DEFFFFFF #F1FFAA00 #F1FFAA00 ================================================ FILE: app/src/test/java/dev/shreyaspatil/covid19notify/ExampleUnitTest.kt ================================================ package dev.shreyaspatil.covid19notify import org.junit.Assert.assertEquals import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.4.21' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Mar 30 19:22:13 IST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ]; do ls=$(ls -ld "$PRG") link=$(expr "$ls" : '.*-> \(.*\)$') if expr "$link" : '/.*' >/dev/null; then PRG="$link" else PRG=$(dirname "$PRG")"/$link" fi done SAVED="$(pwd)" cd "$(dirname \"$PRG\")/" >/dev/null APP_HOME="$(pwd -P)" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=$(basename "$0") # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn() { echo "$*" } die() { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$(uname)" in CYGWIN*) cygwin=true ;; Darwin*) darwin=true ;; MINGW*) msys=true ;; NONSTOP*) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ]; then if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ]; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then MAX_FD_LIMIT=$(ulimit -H -n) if [ $? -eq 0 ]; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ]; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin; then APP_HOME=$(cygpath --path --mixed "$APP_HOME") CLASSPATH=$(cygpath --path --mixed "$CLASSPATH") JAVACMD=$(cygpath --unix "$JAVACMD") # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null) SEP="" for dir in $ROOTDIRSRAW; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ]; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@"; do CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -) CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg") else eval $(echo args$i)="\"$arg\"" fi i=$((i + 1)) done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save() { for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ rootProject.name='Covid19Notify' include ':app'