├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml └── jarRepositories.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── google-services.json ├── proguard-rules.pro ├── schemas │ └── com.waseefakhtar.doseapp.data.MedicationDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ └── 4.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── waseefakhtar │ │ └── doseapp │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── waseefakhtar │ │ │ └── doseapp │ │ │ ├── App.kt │ │ │ ├── DoseApp.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MedicationNotificationReceiver.kt │ │ │ ├── MedicationNotificationService.kt │ │ │ ├── analytics │ │ │ ├── AnalyticsEvents.kt │ │ │ └── AnalyticsHelper.kt │ │ │ ├── core │ │ │ └── navigation │ │ │ │ ├── DoseNavigationDestination.kt │ │ │ │ └── NavigationConstants.kt │ │ │ ├── data │ │ │ ├── Converters.kt │ │ │ ├── MedicationDao.kt │ │ │ ├── MedicationDatabase.kt │ │ │ ├── entity │ │ │ │ └── MedicationEntity.kt │ │ │ ├── mapper │ │ │ │ └── MedicationMapper.kt │ │ │ └── repository │ │ │ │ └── MedicationRepositoryImpl.kt │ │ │ ├── di │ │ │ ├── AnalyticsHelperModule.kt │ │ │ ├── MedicationDataModule.kt │ │ │ └── NotificationModule.kt │ │ │ ├── domain │ │ │ ├── model │ │ │ │ └── Medication.kt │ │ │ └── repository │ │ │ │ └── MedicationRepository.kt │ │ │ ├── extension │ │ │ ├── DateExtension.kt │ │ │ └── DateExtensions.kt │ │ │ ├── feature │ │ │ ├── addmedication │ │ │ │ ├── AddMedicationRoute.kt │ │ │ │ ├── DateRangePickerDialog.kt │ │ │ │ ├── EndDatePickerDialog.kt │ │ │ │ ├── TimePickerDialogComponent.kt │ │ │ │ ├── model │ │ │ │ │ └── CalendarInformation.kt │ │ │ │ ├── navigation │ │ │ │ │ └── AddMedicationDestination.kt │ │ │ │ └── viewmodel │ │ │ │ │ └── AddMedicationViewModel.kt │ │ │ ├── calendar │ │ │ │ ├── CalendarScreen.kt │ │ │ │ ├── navigation │ │ │ │ │ └── CalendarNavigation.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── CalendarState.kt │ │ │ │ │ └── CalendarViewModel.kt │ │ │ ├── history │ │ │ │ ├── HistoryNavigation.kt │ │ │ │ ├── HistoryScreen.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── HistoryState.kt │ │ │ │ │ └── HistoryViewModel.kt │ │ │ ├── home │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── MedicationCard.kt │ │ │ │ ├── data │ │ │ │ │ └── CalendarDataSource.kt │ │ │ │ ├── model │ │ │ │ │ └── CalendarModel.kt │ │ │ │ ├── navigation │ │ │ │ │ └── HomeDestination.kt │ │ │ │ ├── usecase │ │ │ │ │ ├── GetMedicationsUseCase.kt │ │ │ │ │ └── UpdateMedicationUseCase.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── HomeState.kt │ │ │ │ │ └── HomeViewModel.kt │ │ │ ├── medicationconfirm │ │ │ │ ├── MedicationConfirmRoute.kt │ │ │ │ ├── navigation │ │ │ │ │ └── MedicationConfirmDestination.kt │ │ │ │ ├── usecase │ │ │ │ │ └── AddMedicationUseCase.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── MedicationConfirmState.kt │ │ │ │ │ └── MedicationConfirmViewModel.kt │ │ │ └── medicationdetail │ │ │ │ ├── MedicationDetailDestination.kt │ │ │ │ ├── MedicationDetailRoute.kt │ │ │ │ ├── usecase │ │ │ │ └── GetMedicationUseCase.kt │ │ │ │ └── viewmodel │ │ │ │ └── MedicationDetailViewModel.kt │ │ │ ├── navigation │ │ │ ├── DoseNavHost.kt │ │ │ └── DoseTopLevelNavigation.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── util │ │ │ ├── DurationFormatter.kt │ │ │ ├── FrequencyUtil.kt │ │ │ ├── MedicationType.kt │ │ │ ├── SnackbarUtil.kt │ │ │ ├── TimeUtils.kt │ │ │ └── Utils.kt │ └── res │ │ ├── drawable-xxxhdpi │ │ └── doctor.png │ │ ├── drawable │ │ ├── ic_capsule.xml │ │ ├── ic_dose.xml │ │ ├── ic_drops.xml │ │ ├── ic_gel.xml │ │ ├── ic_monochrome.xml │ │ ├── ic_spray.xml │ │ ├── ic_syrup.xml │ │ └── ic_tablet.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── resources.properties │ │ ├── values-b+it │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-fa │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── waseefakhtar │ └── doseapp │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── docs ├── demo │ └── demo.mp4 ├── images │ ├── dose-splash-2.png │ ├── dose-splash-3.jpg │ ├── dose-splash.png │ └── play-store.png ├── release │ └── app.apk └── screenshots │ ├── AddMedication.png │ ├── Home.png │ └── MedicationConfirm.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: waseefakhtar 4 | patreon: waseefakhtar 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: waseefakhtar 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: waseefakhtar 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://wise.com/pay#c5WqArsOsqN1YtRJIZ3W9RiN910'] 14 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Cache Gradle and wrapper 24 | uses: actions/cache@v3 25 | with: 26 | path: | 27 | ~/.gradle/caches 28 | ~/.gradle/wrapper 29 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} 30 | restore-keys: | 31 | ${{ runner.os }}-gradle- 32 | 33 | - name: Grant execute permission for gradlew 34 | run: chmod +x gradlew 35 | 36 | - name: Build with Gradle 37 | run: ./gradlew build 38 | 39 | - name: Run tests 40 | run: ./gradlew test 41 | 42 | - name: Lint check 43 | run: ./gradlew ktlintCheck 44 | 45 | - name: Upload a Build Artifact 46 | uses: actions/upload-artifact@v4 47 | with: 48 | # Artifact name 49 | name: 50 | app.apk 51 | # A file, directory or wildcard pattern that describes what to upload 52 | path: 53 | ./app/build/outputs/apk/debug/app-debug.apk 54 | if-no-files-found: error 55 | 56 | -------------------------------------------------------------------------------- /.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 | local.properties 16 | .idea/ 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | /compiler.xml 5 | /gradle.xml 6 | /misc.xml 7 | /vcs.xml 8 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Dose App -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Waseef Akhtar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Dose App](docs/images/play-store.png "Dose App") 2 | 3 | 4 |

Dose App 💊⏰

5 |

6 | Google 7 | YouTube 8 | Twitter 9 | Blogpost 10 | YouTube Tutorial
11 | Google Play 12 | API 13 | Build Status 14 | Contributors 15 |

16 | 17 |

18 | Dose is a medication reminder app for Android, designed to help you stay on top of your health by reminding you to take your medications on time — Made with Jetpack Compose, Material Design 3, Room, Navigation Components, Kotlin Coroutines, Hilt, Firebase using the recommended Android Architecture Guidelines. 19 |

20 |

21 | I’m building it in public. So the idea is for everyone to contribute, leave feedback, suggest ideas, and collaborate! 22 |

23 | 24 |

25 | Got any crazy new ideas? Head over to the Discussions tab and start a new discussion. 26 |

27 | 28 | ## MAD Score 29 | 30 | 31 | 32 | 33 | 34 | ## IDE Version 35 | Android Studio Koala | 2024.1.1 36 | 37 | ## Contributions 38 | 39 | If you've found an error in the project, please file an issue. 40 | 41 | Patches are encouraged and may be submitted by forking this project and submitting a pull request. Since this project is still in its very early stages, if your change is substantial, please raise an issue first to discuss it. 42 | 43 | ## License 44 | 45 | Dose App is distributed under the terms of the MIT License. See the 46 | [license](LICENSE) for more information. 47 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.hilt.android) 5 | alias(libs.plugins.kotlin.parcelize) 6 | alias(libs.plugins.ksp) 7 | alias(libs.plugins.google.services) 8 | alias(libs.plugins.firebase.crashlytics) 9 | alias(libs.plugins.kotlin.compose) 10 | } 11 | 12 | android { 13 | compileSdk = libs.versions.compile.sdk.version.get().toInt() 14 | 15 | defaultConfig { 16 | applicationId = "com.waseefakhtar.doseapp.dev" 17 | minSdk = libs.versions.min.sdk.version.get().toInt() 18 | targetSdk = libs.versions.target.sdk.version.get().toInt() 19 | versionCode = libs.versions.version.code.get().toInt() 20 | versionName = libs.versions.version.name.get() 21 | 22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 23 | vectorDrawables { 24 | useSupportLibrary = true 25 | } 26 | } 27 | 28 | androidResources { 29 | generateLocaleConfig = true 30 | } 31 | 32 | buildTypes { 33 | getByName("release") { 34 | isMinifyEnabled = true 35 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 36 | } 37 | } 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_17 40 | targetCompatibility = JavaVersion.VERSION_17 41 | } 42 | kotlinOptions { 43 | jvmTarget = JavaVersion.VERSION_17.toString() 44 | } 45 | buildFeatures { 46 | compose = true 47 | } 48 | composeOptions { 49 | kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() 50 | } 51 | packaging { 52 | resources.excludes.apply { 53 | add("/META-INF/{AL2.0,LGPL2.1}") 54 | } 55 | } 56 | ksp { 57 | arg("room.schemaLocation", "$projectDir/schemas") 58 | } 59 | 60 | namespace = "com.waseefakhtar.doseapp" 61 | } 62 | 63 | dependencies { 64 | implementation(libs.androidx.core.ktx) 65 | implementation(libs.compose.ui) 66 | implementation(libs.compose.material3) 67 | implementation(libs.compose.navigation) 68 | implementation(libs.compose.fundation) 69 | 70 | implementation(libs.compose.preview) 71 | 72 | implementation(libs.compose.activity) 73 | 74 | // Lifecycle 75 | implementation(libs.lifecycle.runtime.ktx) 76 | implementation(libs.lifecycle.viewmodel.compose) 77 | 78 | // Hilt 79 | implementation(libs.hilt.android) 80 | ksp(libs.hilt.compiler) 81 | ksp(libs.hilt.androidx.compiler) 82 | implementation(libs.hilt.navigation.compose) 83 | 84 | // Gson 85 | implementation(libs.gson) 86 | 87 | // Room 88 | implementation(libs.room.runtime) 89 | implementation(libs.room.ktx) 90 | ksp(libs.room.compiler) 91 | 92 | // OkHttp 93 | implementation(platform(libs.okhttp.bom)) 94 | implementation(libs.okhttp) 95 | implementation(libs.okhttp.logging.interceptor) 96 | 97 | // Firebase 98 | implementation(platform(libs.firebase.bom)) 99 | implementation(libs.firebase.analytics) 100 | implementation(libs.firebase.crashlytics) 101 | 102 | // Accompanist 103 | implementation(libs.accompanist.permission) 104 | 105 | testImplementation(libs.junit) 106 | androidTestImplementation(libs.junit.ext) 107 | androidTestImplementation(libs.espresso.core) 108 | androidTestImplementation(libs.compose.junit.ui) 109 | debugImplementation(libs.compose.ui.tooling.debug) 110 | debugImplementation(libs.compose.ui.test.manifest) 111 | } 112 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "995841033966", 4 | "project_id": "dose-7d65b", 5 | "storage_bucket": "dose-7d65b.firebasestorage.app" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:995841033966:android:f9fa2a4c671ec4993815ff", 11 | "android_client_info": { 12 | "package_name": "com.waseefakhtar.doseapp" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "995841033966-bakjl4d4pno6501ep80jt2k5ebckb07r.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyDGu4SyesfQSfxSehK-5TZF8nj0Rzgn-UA" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "995841033966-bakjl4d4pno6501ep80jt2k5ebckb07r.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | }, 37 | { 38 | "client_info": { 39 | "mobilesdk_app_id": "1:995841033966:android:ef8d3bbc8a6a40953815ff", 40 | "android_client_info": { 41 | "package_name": "com.waseefakhtar.doseapp.dev" 42 | } 43 | }, 44 | "oauth_client": [ 45 | { 46 | "client_id": "995841033966-bakjl4d4pno6501ep80jt2k5ebckb07r.apps.googleusercontent.com", 47 | "client_type": 3 48 | } 49 | ], 50 | "api_key": [ 51 | { 52 | "current_key": "AIzaSyDGu4SyesfQSfxSehK-5TZF8nj0Rzgn-UA" 53 | } 54 | ], 55 | "services": { 56 | "appinvite_service": { 57 | "other_platform_oauth_client": [ 58 | { 59 | "client_id": "995841033966-bakjl4d4pno6501ep80jt2k5ebckb07r.apps.googleusercontent.com", 60 | "client_type": 3 61 | } 62 | ] 63 | } 64 | } 65 | } 66 | ], 67 | "configuration_version": "1" 68 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/schemas/com.waseefakhtar.doseapp.data.MedicationDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "14df744c3ccccf8ea32acc3f486529be", 6 | "entities": [ 7 | { 8 | "tableName": "MedicationEntity", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `dosage` INTEGER NOT NULL, `recurrence` TEXT NOT NULL, `endDate` INTEGER NOT NULL, `timesOfDay` TEXT NOT NULL, `medicationTaken` INTEGER NOT NULL, `date` INTEGER NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "name", 19 | "columnName": "name", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "dosage", 25 | "columnName": "dosage", 26 | "affinity": "INTEGER", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "recurrence", 31 | "columnName": "recurrence", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "endDate", 37 | "columnName": "endDate", 38 | "affinity": "INTEGER", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "timesOfDay", 43 | "columnName": "timesOfDay", 44 | "affinity": "TEXT", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "medicationTaken", 49 | "columnName": "medicationTaken", 50 | "affinity": "INTEGER", 51 | "notNull": true 52 | }, 53 | { 54 | "fieldPath": "date", 55 | "columnName": "date", 56 | "affinity": "INTEGER", 57 | "notNull": true 58 | } 59 | ], 60 | "primaryKey": { 61 | "autoGenerate": true, 62 | "columnNames": [ 63 | "id" 64 | ] 65 | }, 66 | "indices": [], 67 | "foreignKeys": [] 68 | } 69 | ], 70 | "views": [], 71 | "setupQueries": [ 72 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 73 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14df744c3ccccf8ea32acc3f486529be')" 74 | ] 75 | } 76 | } -------------------------------------------------------------------------------- /app/schemas/com.waseefakhtar.doseapp.data.MedicationDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "fbbda184cc33451bf82be63dcd65c2c5", 6 | "entities": [ 7 | { 8 | "tableName": "MedicationEntity", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `dosage` INTEGER NOT NULL, `recurrence` TEXT NOT NULL, `endDate` INTEGER NOT NULL, `medicationTime` INTEGER NOT NULL, `medicationTaken` INTEGER NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "name", 19 | "columnName": "name", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "dosage", 25 | "columnName": "dosage", 26 | "affinity": "INTEGER", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "recurrence", 31 | "columnName": "recurrence", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "endDate", 37 | "columnName": "endDate", 38 | "affinity": "INTEGER", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "medicationTime", 43 | "columnName": "medicationTime", 44 | "affinity": "INTEGER", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "medicationTaken", 49 | "columnName": "medicationTaken", 50 | "affinity": "INTEGER", 51 | "notNull": true 52 | } 53 | ], 54 | "primaryKey": { 55 | "autoGenerate": true, 56 | "columnNames": [ 57 | "id" 58 | ] 59 | }, 60 | "indices": [], 61 | "foreignKeys": [] 62 | } 63 | ], 64 | "views": [], 65 | "setupQueries": [ 66 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 67 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fbbda184cc33451bf82be63dcd65c2c5')" 68 | ] 69 | } 70 | } -------------------------------------------------------------------------------- /app/schemas/com.waseefakhtar.doseapp.data.MedicationDatabase/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 3, 5 | "identityHash": "ccdaecb2dc04ba66051d7f529be84226", 6 | "entities": [ 7 | { 8 | "tableName": "MedicationEntity", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `dosage` INTEGER NOT NULL, `recurrence` TEXT NOT NULL, `startDate` INTEGER, `endDate` INTEGER NOT NULL, `medicationTaken` INTEGER NOT NULL, `medicationTime` INTEGER NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "name", 19 | "columnName": "name", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "dosage", 25 | "columnName": "dosage", 26 | "affinity": "INTEGER", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "recurrence", 31 | "columnName": "recurrence", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "startDate", 37 | "columnName": "startDate", 38 | "affinity": "INTEGER", 39 | "notNull": false 40 | }, 41 | { 42 | "fieldPath": "endDate", 43 | "columnName": "endDate", 44 | "affinity": "INTEGER", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "medicationTaken", 49 | "columnName": "medicationTaken", 50 | "affinity": "INTEGER", 51 | "notNull": true 52 | }, 53 | { 54 | "fieldPath": "medicationTime", 55 | "columnName": "medicationTime", 56 | "affinity": "INTEGER", 57 | "notNull": true 58 | } 59 | ], 60 | "primaryKey": { 61 | "autoGenerate": true, 62 | "columnNames": [ 63 | "id" 64 | ] 65 | }, 66 | "indices": [], 67 | "foreignKeys": [] 68 | } 69 | ], 70 | "views": [], 71 | "setupQueries": [ 72 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 73 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ccdaecb2dc04ba66051d7f529be84226')" 74 | ] 75 | } 76 | } -------------------------------------------------------------------------------- /app/schemas/com.waseefakhtar.doseapp.data.MedicationDatabase/4.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 4, 5 | "identityHash": "c2163e2bc78a8134c2f0fd7089606e0b", 6 | "entities": [ 7 | { 8 | "tableName": "MedicationEntity", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `dosage` INTEGER NOT NULL, `recurrence` TEXT NOT NULL, `startDate` INTEGER, `endDate` INTEGER NOT NULL, `medicationTaken` INTEGER NOT NULL, `medicationTime` INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT 'TABLET')", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "name", 19 | "columnName": "name", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "dosage", 25 | "columnName": "dosage", 26 | "affinity": "INTEGER", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "recurrence", 31 | "columnName": "recurrence", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "startDate", 37 | "columnName": "startDate", 38 | "affinity": "INTEGER", 39 | "notNull": false 40 | }, 41 | { 42 | "fieldPath": "endDate", 43 | "columnName": "endDate", 44 | "affinity": "INTEGER", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "medicationTaken", 49 | "columnName": "medicationTaken", 50 | "affinity": "INTEGER", 51 | "notNull": true 52 | }, 53 | { 54 | "fieldPath": "medicationTime", 55 | "columnName": "medicationTime", 56 | "affinity": "INTEGER", 57 | "notNull": true 58 | }, 59 | { 60 | "fieldPath": "type", 61 | "columnName": "type", 62 | "affinity": "TEXT", 63 | "notNull": true, 64 | "defaultValue": "'TABLET'" 65 | } 66 | ], 67 | "primaryKey": { 68 | "autoGenerate": true, 69 | "columnNames": [ 70 | "id" 71 | ] 72 | }, 73 | "indices": [], 74 | "foreignKeys": [] 75 | } 76 | ], 77 | "views": [], 78 | "setupQueries": [ 79 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 80 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c2163e2bc78a8134c2f0fd7089606e0b')" 81 | ] 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/waseefakhtar/doseapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.waseefakhtar.doseapp", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/App.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp 2 | 3 | import android.app.Application 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.os.Build 8 | import dagger.hilt.android.HiltAndroidApp 9 | 10 | @HiltAndroidApp 11 | class App : Application() { 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | createNotificationChannel() 16 | } 17 | 18 | private fun createNotificationChannel() { 19 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 20 | val channel = NotificationChannel( 21 | MedicationNotificationService.MEDICATION_CHANNEL_ID, 22 | getString(R.string.medication_reminder), 23 | NotificationManager.IMPORTANCE_HIGH 24 | ) 25 | channel.description = getString(R.string.notifications_for_medication_reminder) 26 | 27 | val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 28 | notificationManager.createNotificationChannel(channel) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/DoseApp.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.slideInVertically 5 | import androidx.compose.animation.slideOutVertically 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.WindowInsets 9 | import androidx.compose.foundation.layout.WindowInsetsSides 10 | import androidx.compose.foundation.layout.consumeWindowInsets 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.only 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.safeDrawing 15 | import androidx.compose.foundation.layout.windowInsetsPadding 16 | import androidx.compose.material.icons.Icons 17 | import androidx.compose.material.icons.filled.Add 18 | import androidx.compose.material3.ExtendedFloatingActionButton 19 | import androidx.compose.material3.FloatingActionButtonDefaults 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.NavigationBar 23 | import androidx.compose.material3.NavigationBarItem 24 | import androidx.compose.material3.Scaffold 25 | import androidx.compose.material3.Surface 26 | import androidx.compose.material3.Text 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.runtime.mutableStateOf 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.runtime.saveable.rememberSaveable 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.graphics.Color 34 | import androidx.compose.ui.platform.LocalContext 35 | import androidx.compose.ui.res.stringResource 36 | import androidx.compose.ui.tooling.preview.Preview 37 | import androidx.compose.ui.unit.dp 38 | import androidx.compose.ui.zIndex 39 | import androidx.navigation.NavController 40 | import androidx.navigation.NavDestination 41 | import androidx.navigation.NavDestination.Companion.hierarchy 42 | import androidx.navigation.compose.currentBackStackEntryAsState 43 | import androidx.navigation.compose.rememberNavController 44 | import com.waseefakhtar.doseapp.analytics.AnalyticsEvents 45 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 46 | import com.waseefakhtar.doseapp.feature.addmedication.navigation.AddMedicationDestination 47 | import com.waseefakhtar.doseapp.feature.history.HistoryDestination 48 | import com.waseefakhtar.doseapp.feature.home.navigation.HomeDestination 49 | import com.waseefakhtar.doseapp.navigation.DoseNavHost 50 | import com.waseefakhtar.doseapp.navigation.DoseTopLevelNavigation 51 | import com.waseefakhtar.doseapp.navigation.TOP_LEVEL_DESTINATIONS 52 | import com.waseefakhtar.doseapp.navigation.TopLevelDestination 53 | import com.waseefakhtar.doseapp.ui.theme.DoseAppTheme 54 | import com.waseefakhtar.doseapp.util.SnackbarUtil 55 | 56 | @Composable 57 | fun DoseApp( 58 | analyticsHelper: AnalyticsHelper 59 | ) { 60 | DoseAppTheme { 61 | // A surface container using the 'background' color from the theme 62 | Surface( 63 | modifier = Modifier.fillMaxSize(), 64 | color = MaterialTheme.colorScheme.background 65 | ) { 66 | 67 | val navController = rememberNavController() 68 | val doseTopLevelNavigation = remember(navController) { 69 | DoseTopLevelNavigation(navController) 70 | } 71 | 72 | val navBackStackEntry by navController.currentBackStackEntryAsState() 73 | val currentDestination = navBackStackEntry?.destination 74 | 75 | val bottomBarVisibility = rememberSaveable { (mutableStateOf(true)) } 76 | val fabVisibility = rememberSaveable { (mutableStateOf(true)) } 77 | 78 | Scaffold( 79 | modifier = Modifier.padding(16.dp, 0.dp), 80 | containerColor = Color.Transparent, 81 | contentColor = MaterialTheme.colorScheme.onBackground, 82 | floatingActionButton = { 83 | 84 | AnimatedVisibility( 85 | visible = fabVisibility.value, 86 | enter = slideInVertically(initialOffsetY = { it }), 87 | exit = slideOutVertically(targetOffsetY = { it }), 88 | content = { 89 | DoseFAB(navController, analyticsHelper) 90 | } 91 | ) 92 | }, 93 | bottomBar = { 94 | Box { 95 | SnackbarUtil.SnackbarWithoutScaffold( 96 | SnackbarUtil.getSnackbarMessage().component1(), 97 | SnackbarUtil.isSnackbarVisible().component1() 98 | ) { 99 | SnackbarUtil.hideSnackbar() 100 | } 101 | AnimatedVisibility( 102 | visible = bottomBarVisibility.value, 103 | enter = slideInVertically(initialOffsetY = { it }), 104 | exit = slideOutVertically(targetOffsetY = { it }), 105 | content = { 106 | DoseBottomBar( 107 | onNavigateToTopLevelDestination = doseTopLevelNavigation::navigateTo, 108 | currentDestination = currentDestination, 109 | analyticsHelper = analyticsHelper 110 | ) 111 | } 112 | ) 113 | } 114 | } 115 | ) { padding -> 116 | Row( 117 | Modifier 118 | .fillMaxSize() 119 | .windowInsetsPadding( 120 | WindowInsets.safeDrawing.only( 121 | WindowInsetsSides.Horizontal 122 | ) 123 | ) 124 | ) { 125 | 126 | DoseNavHost( 127 | bottomBarVisibility = bottomBarVisibility, 128 | fabVisibility = fabVisibility, 129 | navController = navController, 130 | modifier = Modifier 131 | .padding(padding) 132 | .consumeWindowInsets(padding) 133 | .zIndex(1f) 134 | ) 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | @Composable 142 | private fun DoseBottomBar( 143 | onNavigateToTopLevelDestination: (TopLevelDestination) -> Unit, 144 | currentDestination: NavDestination?, 145 | analyticsHelper: AnalyticsHelper 146 | ) { 147 | // Wrap the navigation bar in a surface so the color behind the system 148 | // navigation is equal to the container color of the navigation bar. 149 | Surface(color = MaterialTheme.colorScheme.surface) { 150 | NavigationBar( 151 | modifier = Modifier.windowInsetsPadding( 152 | WindowInsets.safeDrawing.only( 153 | WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom 154 | ) 155 | ), 156 | tonalElevation = 0.dp 157 | ) { 158 | 159 | TOP_LEVEL_DESTINATIONS.forEach { destination -> 160 | val selected = 161 | currentDestination?.hierarchy?.any { it.route == destination.route } == true 162 | NavigationBarItem( 163 | selected = selected, 164 | onClick = 165 | { 166 | trackTabClicked(analyticsHelper, destination.route) 167 | onNavigateToTopLevelDestination(destination) 168 | }, 169 | icon = { 170 | Icon( 171 | if (selected) { 172 | destination.selectedIcon 173 | } else { 174 | destination.unselectedIcon 175 | }, 176 | contentDescription = null 177 | ) 178 | }, 179 | label = { Text(stringResource(destination.iconTextId)) } 180 | ) 181 | } 182 | } 183 | } 184 | } 185 | 186 | private fun trackTabClicked(analyticsHelper: AnalyticsHelper, route: String) { 187 | if (route == HomeDestination.route) { 188 | analyticsHelper.logEvent(AnalyticsEvents.HOME_TAB_CLICKED) 189 | } 190 | 191 | if (route == HistoryDestination.route) { 192 | analyticsHelper.logEvent(AnalyticsEvents.HISTORY_TAB_CLICKED) 193 | } 194 | } 195 | 196 | @Composable 197 | fun DoseFAB(navController: NavController, analyticsHelper: AnalyticsHelper) { 198 | ExtendedFloatingActionButton( 199 | text = { Text(text = stringResource(id = R.string.add_medication)) }, 200 | icon = { 201 | Icon( 202 | imageVector = Icons.Default.Add, 203 | contentDescription = stringResource(R.string.add) 204 | ) 205 | }, 206 | onClick = { 207 | analyticsHelper.logEvent(AnalyticsEvents.ADD_MEDICATION_CLICKED_FAB) 208 | navController.navigate(AddMedicationDestination.route) 209 | }, 210 | elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp), 211 | containerColor = MaterialTheme.colorScheme.tertiary 212 | ) 213 | } 214 | 215 | @Preview(showBackground = true) 216 | @Composable 217 | fun DefaultPreview() { 218 | val context = LocalContext.current 219 | DoseAppTheme { 220 | DoseApp(analyticsHelper = AnalyticsHelper(context = context)) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import com.waseefakhtar.doseapp.analytics.AnalyticsEvents 8 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 9 | import dagger.hilt.android.AndroidEntryPoint 10 | import javax.inject.Inject 11 | 12 | @AndroidEntryPoint 13 | class MainActivity : ComponentActivity() { 14 | 15 | @Inject 16 | lateinit var analyticsHelper: AnalyticsHelper 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContent { 21 | DoseApp(analyticsHelper = analyticsHelper) 22 | } 23 | parseIntent(intent) 24 | } 25 | 26 | private fun parseIntent(intent: Intent?) { 27 | val isMedicationNotification = intent?.getBooleanExtra(MEDICATION_NOTIFICATION, false) ?: false 28 | if (isMedicationNotification) { 29 | analyticsHelper.logEvent(AnalyticsEvents.REMINDER_NOTIFICATION_CLICKED) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/MedicationNotificationReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp 2 | 3 | import android.app.NotificationManager 4 | import android.app.PendingIntent 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.net.Uri 9 | import androidx.core.app.NotificationCompat 10 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 11 | import com.waseefakhtar.doseapp.domain.model.Medication 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import javax.inject.Inject 14 | 15 | const val MEDICATION_INTENT = "medication_intent" 16 | const val MEDICATION_NOTIFICATION = "medication_notification" 17 | 18 | @AndroidEntryPoint 19 | class MedicationNotificationReceiver : BroadcastReceiver() { 20 | 21 | @Inject 22 | lateinit var analyticsHelper: AnalyticsHelper 23 | 24 | override fun onReceive(context: Context?, intent: Intent?) { 25 | context?.let { 26 | intent?.getParcelableExtra(MEDICATION_INTENT)?.let { medication -> 27 | showNotification(it, medication) 28 | } 29 | } 30 | } 31 | 32 | private fun showNotification(context: Context, medication: Medication) { 33 | // Create deep link intent for notification click 34 | val activityIntent = Intent(context, MainActivity::class.java).apply { 35 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 36 | data = Uri.parse("doseapp://medication/${medication.id}") 37 | } 38 | 39 | val activityPendingIntent = PendingIntent.getActivity( 40 | context, 41 | medication.id.toInt(), 42 | activityIntent, 43 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 44 | ) 45 | 46 | val notification = NotificationCompat.Builder( 47 | context, 48 | MedicationNotificationService.MEDICATION_CHANNEL_ID 49 | ) 50 | .setSmallIcon(R.drawable.ic_dose) 51 | .setContentTitle(context.getString(R.string.medication_reminder)) 52 | .setContentText(context.getString(R.string.medication_reminder_time, medication.name)) 53 | .setAutoCancel(true) 54 | .setContentIntent(activityPendingIntent) 55 | .build() 56 | 57 | val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 58 | notificationManager.notify(medication.id.toInt(), notification) 59 | 60 | analyticsHelper.trackNotificationShown(medication) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/MedicationNotificationService.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp 2 | 3 | import android.app.AlarmManager 4 | import android.app.PendingIntent 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.os.Build 8 | import com.google.firebase.crashlytics.FirebaseCrashlytics 9 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 10 | import com.waseefakhtar.doseapp.domain.model.Medication 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import javax.inject.Inject 13 | 14 | class MedicationNotificationService @Inject constructor( 15 | @ApplicationContext private val context: Context 16 | ) { 17 | fun scheduleNotification(medication: Medication, analyticsHelper: AnalyticsHelper) { 18 | val alarmIntent = Intent(context, MedicationNotificationReceiver::class.java).apply { 19 | putExtra(MEDICATION_INTENT, medication) 20 | } 21 | 22 | val alarmPendingIntent = PendingIntent.getBroadcast( 23 | context, 24 | medication.id.toInt(), 25 | alarmIntent, 26 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 27 | ) 28 | 29 | val alarmService = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 30 | val time = medication.medicationTime.time 31 | 32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 33 | try { 34 | alarmService.setExactAndAllowWhileIdle( 35 | AlarmManager.RTC_WAKEUP, 36 | time, 37 | alarmPendingIntent 38 | ) 39 | } catch (exception: SecurityException) { 40 | FirebaseCrashlytics.getInstance().recordException(exception) 41 | } 42 | } 43 | 44 | analyticsHelper.trackNotificationScheduled(medication) 45 | } 46 | 47 | companion object { 48 | const val MEDICATION_CHANNEL_ID = "medication_channel" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/analytics/AnalyticsEvents.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.analytics 2 | 3 | object AnalyticsEvents { 4 | 5 | const val NOTIFICATION_PERMISSION_DIALOG_SHOWN = "notification_permission_dialog_shown" 6 | const val NOTIFICATION_PERMISSION_DIALOG_DISMISSED = "notification_permission_dialog_shown" 7 | const val NOTIFICATION_PERMISSION_DIALOG_ALLOW_CLICKED = "notification_permission_dialog_allow_clicked" 8 | const val NOTIFICATION_PERMISSION_GRANTED = "notification_permission_granted" 9 | const val NOTIFICATION_PERMISSION_REFUSED = "notification_permission_refused" 10 | 11 | const val ALARM_PERMISSION_DIALOG_SHOWN = "alarm_permission_dialog_shown" 12 | const val ALARM_PERMISSION_DIALOG_DISMISSED = "alarm_permission_dialog_shown" 13 | const val ALARM_PERMISSION_DIALOG_ALLOW_CLICKED = "alarm_permission_dialog_allow_clicked" 14 | const val ALARM_PERMISSION_GRANTED = "alarm_permission_granted" 15 | const val ALARM_PERMISSION_REFUSED = "alarm_permission_refused" 16 | 17 | const val EMPTY_CARD_SHOWN = "empty_card_shown" 18 | 19 | const val ADD_MEDICATION_CLICKED_EMPTY_CARD = "add_medication_clicked_empty_card" 20 | const val ADD_MEDICATION_CLICKED_DAILY_OVERVIEW = "add_medication_clicked_daily_overview" 21 | const val ADD_MEDICATION_CLICKED_FAB = "add_medication_clicked_fab" 22 | 23 | const val TAKE_MEDICATION_CLICKED = "take_medication_clicked" 24 | 25 | const val MEDICATION_NOTIFICATION_SHOWN = "medication_notification_shown" 26 | const val MEDICATION_NOTIFICATION_SCHEDULED = "medication_notification_scheduled" 27 | 28 | const val MEDICATIONS_SAVED = "medications_saved" 29 | 30 | const val MEDICATION_CONFIRM_ON_BACK_CLICKED = "medications_confirm_on_back_clicked" 31 | const val MEDICATION_CONFIRM_ON_CONFIRM_CLICKED = "medications_confirm_on_confirm_clicked" 32 | 33 | const val ADD_MEDICATION_ON_BACK_CLICKED = "add_medication_on_back_clicked" 34 | const val ADD_MED_VALUE_INVALIDATED = "add_med_%s_invalidated" 35 | const val ADD_MED_NAVIGATING_TO_MED_CONFIRM = "add_med_navigating_to_med_confirm" 36 | const val ADD_MEDICATION_ADD_TIME_CLICKED = "add_medication_add_time_clicked" 37 | const val ADD_MEDICATION_DELETE_TIME_CLICKED = "add_medication_delete_time_clicked" 38 | const val ADD_MEDICATION_NEW_TIME_SELECTED = "add_medication_new_time_selected" 39 | 40 | const val REMINDER_NOTIFICATION_CLICKED = "reminder_notification_clicked" 41 | 42 | const val MEDICATION_DETAIL_ON_BACK_CLICKED = "medication_detail_on_back_clicked" 43 | const val MEDICATION_DETAIL_TAKEN_CLICKED = "medication_detail_taken_clicked" 44 | const val MEDICATION_DETAIL_SKIPPED_CLICKED = "medication_detail_skipped_clicked" 45 | const val MEDICATION_DETAIL_DONE_CLICKED = "medication_detail_done_clicked" 46 | 47 | const val HOME_TAB_CLICKED = "home_tab_clicked" 48 | const val HISTORY_TAB_CLICKED = "history_tab_clicked" 49 | 50 | const val HOME_CALENDAR_PREVIOUS_WEEK_CLICKED = "home_calendar_previous_week_clicked" 51 | const val HOME_CALENDAR_NEXT_WEEK_CLICKED = "home_calendar_next_week_clicked" 52 | const val HOME_NEW_DATE_SELECTED = "home_new_date_clicked" 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/analytics/AnalyticsHelper.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.analytics 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.core.os.bundleOf 6 | import com.google.firebase.analytics.FirebaseAnalytics 7 | import com.waseefakhtar.doseapp.domain.model.Medication 8 | import com.waseefakhtar.doseapp.extension.toFormattedDateString 9 | import java.util.Date 10 | 11 | private const val MEDICATION_TIME = "medication_time" 12 | private const val MEDICATION_END_DATE = "medication_end_date" 13 | private const val NOTIFICATION_TIME = "notification_time" 14 | 15 | class AnalyticsHelper( 16 | context: Context 17 | ) { 18 | private val firebaseAnalytics = FirebaseAnalytics.getInstance(context) 19 | 20 | fun trackNotificationShown(medication: Medication) { 21 | val params = bundleOf( 22 | MEDICATION_TIME to medication.medicationTime.toFormattedDateString(), 23 | MEDICATION_END_DATE to medication.endDate.toFormattedDateString(), 24 | NOTIFICATION_TIME to Date().toFormattedDateString() 25 | ) 26 | logEvent(AnalyticsEvents.MEDICATION_NOTIFICATION_SHOWN, params) 27 | } 28 | 29 | fun trackNotificationScheduled(medication: Medication) { 30 | val params = bundleOf( 31 | MEDICATION_TIME to medication.medicationTime.toFormattedDateString(), 32 | MEDICATION_END_DATE to medication.endDate.toFormattedDateString(), 33 | NOTIFICATION_TIME to Date().toFormattedDateString() 34 | ) 35 | logEvent(AnalyticsEvents.MEDICATION_NOTIFICATION_SCHEDULED, params) 36 | } 37 | 38 | fun logEvent(eventName: String, params: Bundle? = null) { 39 | firebaseAnalytics.logEvent(eventName, params) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/core/navigation/DoseNavigationDestination.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.core.navigation 2 | 3 | /** 4 | * Interface for describing the Now in Android navigation destinations 5 | */ 6 | 7 | interface DoseNavigationDestination { 8 | /** 9 | * Defines a specific route this destination belongs to. 10 | * Route is a String that defines the path to your composable. 11 | * You can think of it as an implicit deep link that leads to a specific destination. 12 | * Each destination should have a unique route. 13 | */ 14 | val route: String 15 | 16 | /** 17 | * Defines a specific destination ID. 18 | * This is needed when using nested graphs via the navigation DLS, to differentiate a specific 19 | * destination's route from the route of the entire nested graph it belongs to. 20 | */ 21 | val destination: String 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/core/navigation/NavigationConstants.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.core.navigation 2 | 3 | object NavigationConstants { 4 | const val MEDICATION_ID = "medicationId" 5 | const val DEEP_LINK_URI_PATTERN = "doseapp://medication/{$MEDICATION_ID}" 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/data/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.data 2 | 3 | import androidx.room.TypeConverter 4 | import java.util.Date 5 | 6 | class Converters { 7 | @TypeConverter 8 | fun fromTimestamp(value: Long?): Date? { 9 | return value?.let { Date(it) } 10 | } 11 | 12 | @TypeConverter 13 | fun dateToTimestamp(date: Date?): Long? { 14 | return date?.time 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/data/MedicationDao.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.data 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Update 9 | import com.waseefakhtar.doseapp.data.entity.MedicationEntity 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | @Dao 13 | interface MedicationDao { 14 | 15 | @Insert(onConflict = OnConflictStrategy.IGNORE) 16 | suspend fun insertMedication(medicationEntity: MedicationEntity): Long 17 | 18 | @Delete 19 | suspend fun deleteMedication(medicationEntity: MedicationEntity) 20 | 21 | @Update(onConflict = OnConflictStrategy.REPLACE) 22 | suspend fun updateMedication(medicationEntity: MedicationEntity) 23 | 24 | @Query( 25 | """ 26 | SELECT * 27 | FROM medicationentity 28 | """ 29 | ) 30 | fun getAllMedications(): Flow> 31 | 32 | @Query( 33 | """ 34 | SELECT * 35 | FROM medicationentity 36 | WHERE strftime('%Y-%m-%d', medicationTime / 1000, 'unixepoch', 'localtime') = :date 37 | ORDER BY medicationTime ASC 38 | """ 39 | ) 40 | fun getMedicationsForDate(date: String): Flow> 41 | 42 | @Query("SELECT * FROM medicationentity WHERE id = :id") 43 | suspend fun getMedicationById(id: Long): MedicationEntity? 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/data/MedicationDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.data 2 | 3 | import androidx.room.AutoMigration 4 | import androidx.room.Database 5 | import androidx.room.DeleteColumn 6 | import androidx.room.RenameColumn 7 | import androidx.room.RoomDatabase 8 | import androidx.room.TypeConverters 9 | import androidx.room.migration.AutoMigrationSpec 10 | import com.waseefakhtar.doseapp.data.entity.MedicationEntity 11 | 12 | @Database( 13 | entities = [MedicationEntity::class], 14 | version = 4, 15 | autoMigrations = [ 16 | AutoMigration(from = 3, to = 4, spec = MedicationDatabase.AutoMigration::class) 17 | ] 18 | ) 19 | @TypeConverters(Converters::class) 20 | abstract class MedicationDatabase : RoomDatabase() { 21 | 22 | abstract val dao: MedicationDao 23 | @DeleteColumn(tableName = "MedicationEntity", columnName = "timesOfDay") 24 | @RenameColumn(tableName = "MedicationEntity", fromColumnName = "date", toColumnName = "medicationTime") 25 | class AutoMigration : AutoMigrationSpec 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/data/entity/MedicationEntity.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.data.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.waseefakhtar.doseapp.util.MedicationType 7 | import java.util.Date 8 | 9 | @Entity 10 | data class MedicationEntity( 11 | @PrimaryKey(autoGenerate = true) 12 | val id: Long = 0, 13 | val name: String, 14 | val dosage: Int, 15 | val recurrence: String, 16 | val startDate: Date?, 17 | val endDate: Date, 18 | val medicationTaken: Boolean, 19 | val medicationTime: Date, 20 | @ColumnInfo(defaultValue = "TABLET") 21 | val type: String = MedicationType.getDefault().name 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/data/mapper/MedicationMapper.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.data.mapper 2 | 3 | import com.waseefakhtar.doseapp.data.entity.MedicationEntity 4 | import com.waseefakhtar.doseapp.domain.model.Medication 5 | import com.waseefakhtar.doseapp.util.MedicationType 6 | import java.util.Date 7 | 8 | fun MedicationEntity.toMedication(): Medication { 9 | return Medication( 10 | id = id, 11 | name = name, 12 | dosage = dosage, 13 | frequency = recurrence, 14 | startDate = startDate ?: Date(), 15 | endDate = endDate, 16 | medicationTime = medicationTime, 17 | medicationTaken = medicationTaken, 18 | type = MedicationType.valueOf(type) 19 | ) 20 | } 21 | 22 | fun Medication.toMedicationEntity(): MedicationEntity { 23 | return MedicationEntity( 24 | id = id, 25 | name = name, 26 | dosage = dosage, 27 | recurrence = frequency, 28 | startDate = startDate, 29 | endDate = endDate, 30 | medicationTime = medicationTime, 31 | medicationTaken = medicationTaken, 32 | type = type.name 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/data/repository/MedicationRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.data.repository 2 | 3 | import com.waseefakhtar.doseapp.data.MedicationDao 4 | import com.waseefakhtar.doseapp.data.mapper.toMedication 5 | import com.waseefakhtar.doseapp.data.mapper.toMedicationEntity 6 | import com.waseefakhtar.doseapp.domain.model.Medication 7 | import com.waseefakhtar.doseapp.domain.repository.MedicationRepository 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flow 10 | import kotlinx.coroutines.flow.map 11 | 12 | class MedicationRepositoryImpl( 13 | private val dao: MedicationDao 14 | ) : MedicationRepository { 15 | 16 | override suspend fun insertMedications(medications: List): Flow> = flow { 17 | val savedIds = medications.map { medication -> 18 | dao.insertMedication(medication.toMedicationEntity()) 19 | } 20 | // Get the saved medications with their IDs 21 | val savedMedications = medications.mapIndexed { index, medication -> 22 | medication.copy(id = savedIds[index]) 23 | } 24 | emit(savedMedications) 25 | } 26 | 27 | override suspend fun deleteMedication(medication: Medication) { 28 | dao.deleteMedication(medication.toMedicationEntity()) 29 | } 30 | 31 | override suspend fun updateMedication(medication: Medication) { 32 | dao.updateMedication(medication.toMedicationEntity()) 33 | } 34 | 35 | override fun getAllMedications(): Flow> { 36 | return dao.getAllMedications().map { entities -> 37 | entities.map { it.toMedication() } 38 | } 39 | } 40 | 41 | override fun getMedicationsForDate(date: String): Flow> { 42 | return dao.getMedicationsForDate( 43 | date = date 44 | ).map { entities -> 45 | entities.map { it.toMedication() } 46 | } 47 | } 48 | 49 | override suspend fun getMedicationById(id: Long): Medication? { 50 | return dao.getMedicationById(id)?.toMedication() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/di/AnalyticsHelperModule.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.di 2 | 3 | import android.content.Context 4 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object AnalyticsHelperModule { 15 | 16 | @Provides 17 | @Singleton 18 | fun provideAnalyticsHelper( 19 | @ApplicationContext context: Context, 20 | ): AnalyticsHelper { 21 | return AnalyticsHelper(context = context) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/di/MedicationDataModule.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.di 2 | 3 | import android.app.Application 4 | import androidx.room.Room 5 | import com.waseefakhtar.doseapp.data.MedicationDatabase 6 | import com.waseefakhtar.doseapp.data.repository.MedicationRepositoryImpl 7 | import com.waseefakhtar.doseapp.domain.repository.MedicationRepository 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import okhttp3.OkHttpClient 13 | import okhttp3.logging.HttpLoggingInterceptor 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object MedicationDataModule { 19 | 20 | @Provides 21 | @Singleton 22 | fun provideOkHttpClient(): OkHttpClient { 23 | return OkHttpClient.Builder() 24 | .addInterceptor( 25 | HttpLoggingInterceptor().apply { 26 | level = HttpLoggingInterceptor.Level.BODY 27 | } 28 | ) 29 | .build() 30 | } 31 | 32 | @Provides 33 | @Singleton 34 | fun provideMedicationDatabase(app: Application): MedicationDatabase { 35 | return Room.databaseBuilder( 36 | app, 37 | MedicationDatabase::class.java, 38 | "medication_db" 39 | ).build() 40 | } 41 | 42 | @Provides 43 | @Singleton 44 | fun provideMedicationRepository( 45 | db: MedicationDatabase 46 | ): MedicationRepository { 47 | return MedicationRepositoryImpl( 48 | dao = db.dao 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/di/NotificationModule.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.di 2 | 3 | import android.content.Context 4 | import com.waseefakhtar.doseapp.MedicationNotificationService 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object NotificationModule { 15 | @Provides 16 | @Singleton 17 | fun provideMedicationNotificationService( 18 | @ApplicationContext context: Context, 19 | ): MedicationNotificationService = MedicationNotificationService(context) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/domain/model/Medication.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.domain.model 2 | 3 | import android.os.Parcelable 4 | import com.waseefakhtar.doseapp.util.MedicationType 5 | import kotlinx.parcelize.Parcelize 6 | import java.util.Date 7 | 8 | @Parcelize 9 | data class Medication( 10 | val id: Long = 0, 11 | val name: String, 12 | val dosage: Int, 13 | val frequency: String, 14 | val startDate: Date, 15 | val endDate: Date, 16 | val medicationTaken: Boolean, 17 | val medicationTime: Date, 18 | val type: MedicationType = MedicationType.getDefault() 19 | ) : Parcelable 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/domain/repository/MedicationRepository.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.domain.repository 2 | 3 | import com.waseefakhtar.doseapp.domain.model.Medication 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface MedicationRepository { 7 | 8 | suspend fun insertMedications(medications: List): Flow> 9 | 10 | suspend fun deleteMedication(medication: Medication) 11 | 12 | suspend fun updateMedication(medication: Medication) 13 | 14 | fun getAllMedications(): Flow> 15 | 16 | fun getMedicationsForDate(date: String): Flow> 17 | 18 | suspend fun getMedicationById(id: Long): Medication? 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/extension/DateExtension.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.extension 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Calendar 5 | import java.util.Date 6 | import java.util.Locale 7 | 8 | fun Date.toFormattedDateString(): String { 9 | val sdf = SimpleDateFormat("EEEE, LLLL dd", Locale.getDefault()) 10 | return sdf.format(this) 11 | } 12 | 13 | fun Date.toFormattedMonthDateString(): String { 14 | val sdf = SimpleDateFormat("MMMM dd", Locale.getDefault()) 15 | return sdf.format(this) 16 | } 17 | 18 | fun Date.toFormattedYearMonthDateString(): String { 19 | val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) 20 | return sdf.format(this) 21 | } 22 | 23 | fun String.toDate(): Date? { 24 | return try { 25 | val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) 26 | sdf.parse(this) 27 | } catch (e: Exception) { 28 | e.printStackTrace() 29 | null 30 | } 31 | } 32 | 33 | fun Date.toFormattedDateShortString(): String { 34 | val sdf = SimpleDateFormat("dd", Locale.getDefault()) 35 | return sdf.format(this) 36 | } 37 | 38 | fun Long.toFormattedDateString(): String { 39 | val sdf = SimpleDateFormat("LLLL dd, yyyy", Locale.getDefault()) 40 | return sdf.format(this) 41 | } 42 | 43 | fun Date.toFormattedTimeString(): String { 44 | val timeFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) 45 | return timeFormat.format(this) 46 | } 47 | 48 | fun Date.hasPassed(): Boolean { 49 | val calendar = Calendar.getInstance() 50 | calendar.add(Calendar.SECOND, -1) 51 | val oneSecondAgo = calendar.time 52 | return time < oneSecondAgo.time 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/extension/DateExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.extension 2 | 3 | import com.waseefakhtar.doseapp.R 4 | 5 | data class Duration( 6 | val primary: Int, 7 | val primaryType: DurationType, 8 | val remainder: Int? = null, 9 | val remainderType: DurationType? = null, 10 | ) 11 | 12 | fun Long.calculateDurationInDays(endMillis: Long): Int = 13 | ((endMillis - this) / (24 * 60 * 60 * 1000)).toInt() + 1 14 | 15 | fun Long.formatDuration(endMillis: Long): Duration { 16 | val totalDays = calculateDurationInDays(endMillis) 17 | 18 | return when { 19 | totalDays >= 365 -> { 20 | val years = totalDays / 365 21 | val remainingDays = totalDays % 365 22 | when { 23 | remainingDays >= 30 -> 24 | Duration( 25 | primary = years, 26 | primaryType = DurationType.YEARS, 27 | remainder = remainingDays / 30, 28 | remainderType = DurationType.MONTHS, 29 | ) 30 | remainingDays > 0 -> 31 | Duration( 32 | primary = years, 33 | primaryType = DurationType.YEARS, 34 | remainder = remainingDays, 35 | remainderType = DurationType.DAYS, 36 | ) 37 | else -> Duration( 38 | primary = years, 39 | primaryType = DurationType.YEARS, 40 | ) 41 | } 42 | } 43 | totalDays >= 30 -> { 44 | val months = totalDays / 30 45 | val remainingDays = totalDays % 30 46 | if (remainingDays > 0) { 47 | Duration( 48 | primary = months, 49 | primaryType = DurationType.MONTHS, 50 | remainder = remainingDays, 51 | remainderType = DurationType.DAYS, 52 | ) 53 | } else { 54 | Duration( 55 | primary = months, 56 | primaryType = DurationType.MONTHS, 57 | ) 58 | } 59 | } 60 | totalDays >= 7 -> { 61 | val weeks = totalDays / 7 62 | val remainingDays = totalDays % 7 63 | if (remainingDays > 0) { 64 | Duration( 65 | primary = weeks, 66 | primaryType = DurationType.WEEKS, 67 | remainder = remainingDays, 68 | remainderType = DurationType.DAYS, 69 | ) 70 | } else { 71 | Duration( 72 | primary = weeks, 73 | primaryType = DurationType.WEEKS, 74 | ) 75 | } 76 | } 77 | else -> Duration( 78 | primary = totalDays, 79 | primaryType = DurationType.DAYS, 80 | ) 81 | } 82 | } 83 | 84 | enum class DurationType( 85 | val pluralResId: Int, 86 | ) { 87 | DAYS(R.plurals.duration_days), 88 | WEEKS(R.plurals.duration_weeks), 89 | MONTHS(R.plurals.duration_months), 90 | YEARS(R.plurals.duration_years), 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/DateRangePickerDialog.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.addmedication 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.DatePickerDialog 6 | import androidx.compose.material3.DateRangePicker 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextButton 11 | import androidx.compose.material3.rememberDateRangePickerState 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import com.waseefakhtar.doseapp.R 18 | import com.waseefakhtar.doseapp.extension.formatDuration 19 | import com.waseefakhtar.doseapp.util.formatDurationText 20 | import java.util.Calendar 21 | 22 | @OptIn(ExperimentalMaterial3Api::class) 23 | @Composable 24 | fun DateRangePickerDialog( 25 | showDialog: Boolean, 26 | startDate: Long?, 27 | endDate: Long?, 28 | onDismiss: () -> Unit, 29 | onDateSelected: (startDate: Long, endDate: Long) -> Unit, 30 | ) { 31 | if (showDialog) { 32 | val today = Calendar.getInstance().timeInMillis 33 | val dateRangePickerState = 34 | rememberDateRangePickerState( 35 | initialSelectedStartDateMillis = if (startDate == 0L) null else startDate, 36 | initialSelectedEndDateMillis = if (endDate == 0L) null else endDate, 37 | initialDisplayedMonthMillis = today, // Show current month by default 38 | ) 39 | 40 | LaunchedEffect( 41 | dateRangePickerState.selectedStartDateMillis, 42 | dateRangePickerState.selectedEndDateMillis, 43 | ) { 44 | if ( 45 | dateRangePickerState.selectedStartDateMillis != null && 46 | dateRangePickerState.selectedEndDateMillis == null 47 | ) { 48 | val newDate = dateRangePickerState.selectedStartDateMillis!! 49 | if (endDate != null && endDate != 0L && newDate > endDate) { 50 | dateRangePickerState.setSelection(startDate!!, newDate) 51 | } 52 | } 53 | } 54 | 55 | DatePickerDialog( 56 | onDismissRequest = onDismiss, 57 | confirmButton = { 58 | TextButton( 59 | enabled = 60 | dateRangePickerState.selectedStartDateMillis != null && 61 | dateRangePickerState.selectedEndDateMillis != null, 62 | onClick = { 63 | dateRangePickerState.selectedStartDateMillis?.let { start -> 64 | dateRangePickerState.selectedEndDateMillis?.let { end -> 65 | onDateSelected(start, end) 66 | } 67 | } 68 | onDismiss() 69 | }, 70 | ) { 71 | Text(stringResource(R.string.ok)) 72 | } 73 | }, 74 | dismissButton = { 75 | TextButton(onClick = onDismiss) { 76 | Text(stringResource(R.string.cancel)) 77 | } 78 | }, 79 | ) { 80 | DateRangePicker( 81 | state = dateRangePickerState, 82 | title = { 83 | Text( 84 | text = stringResource(R.string.select_duration), 85 | modifier = 86 | Modifier.padding( 87 | start = 24.dp, 88 | end = 12.dp, 89 | top = 16.dp, 90 | ), 91 | ) 92 | }, 93 | headline = { 94 | if ( 95 | dateRangePickerState.selectedStartDateMillis != null && 96 | dateRangePickerState.selectedEndDateMillis != null 97 | ) { 98 | val duration = 99 | dateRangePickerState.selectedStartDateMillis!! 100 | .formatDuration(dateRangePickerState.selectedEndDateMillis!!) 101 | Text( 102 | text = formatDurationText(duration), 103 | style = MaterialTheme.typography.headlineSmall, 104 | modifier = 105 | Modifier 106 | .padding( 107 | start = 24.dp, 108 | end = 12.dp, 109 | bottom = 12.dp, 110 | ).fillMaxWidth(), 111 | color = MaterialTheme.colorScheme.primary, 112 | ) 113 | } 114 | }, 115 | ) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/EndDatePickerDialog.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.addmedication 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.Button 5 | import androidx.compose.material3.DatePicker 6 | import androidx.compose.material3.DatePickerDialog 7 | import androidx.compose.material3.DatePickerState 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import com.waseefakhtar.doseapp.R 16 | import com.waseefakhtar.doseapp.extension.toFormattedDateString 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun EndDatePickerDialog( 21 | state: DatePickerState, 22 | shouldDisplay: Boolean, 23 | onConfirmClicked: (selectedDateInMillis: Long) -> Unit, 24 | dismissRequest: () -> Unit 25 | ) { 26 | if (shouldDisplay) { 27 | DatePickerDialog( 28 | onDismissRequest = dismissRequest, 29 | confirmButton = { 30 | Button( 31 | modifier = Modifier.padding(0.dp, 0.dp, 8.dp, 0.dp), 32 | onClick = { 33 | state.selectedDateMillis?.let { 34 | onConfirmClicked(it) 35 | } 36 | dismissRequest() 37 | } 38 | ) { 39 | Text(text = stringResource(R.string.ok)) 40 | } 41 | }, 42 | dismissButton = { 43 | TextButton(onClick = dismissRequest) { 44 | Text(text = stringResource(R.string.cancel)) 45 | } 46 | }, 47 | content = { 48 | DatePicker( 49 | state = state, 50 | showModeToggle = false, 51 | headline = { 52 | state.selectedDateMillis?.toFormattedDateString()?.let { 53 | Text( 54 | modifier = Modifier.padding(start = 16.dp), 55 | text = it 56 | ) 57 | } 58 | } 59 | ) 60 | } 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/TimePickerDialogComponent.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.addmedication 2 | 3 | import android.app.TimePickerDialog 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalContext 6 | import com.waseefakhtar.doseapp.feature.addmedication.model.CalendarInformation 7 | import java.util.Calendar 8 | 9 | @Composable 10 | fun TimePickerDialogComponent( 11 | showDialog: Boolean, 12 | selectedDate: CalendarInformation, 13 | onSelectedTime: (selectedDate: CalendarInformation) -> Unit 14 | ) { 15 | val listener = setUpOnTimeSetListener(onSelectedTime) 16 | val timePickerDialog = getTimePickerDialog(selectedDate, listener) 17 | if (showDialog) { 18 | timePickerDialog.show() 19 | } 20 | } 21 | 22 | private fun setUpOnTimeSetListener( 23 | onSelectedTime: (selectedDate: CalendarInformation) -> Unit 24 | ): TimePickerDialog.OnTimeSetListener { 25 | return TimePickerDialog.OnTimeSetListener { timePicker, hourOfDay, minute -> 26 | val newDate = Calendar.getInstance().apply { 27 | set(Calendar.HOUR_OF_DAY, hourOfDay) 28 | set(Calendar.MINUTE, minute) 29 | } 30 | onSelectedTime(CalendarInformation(newDate)) 31 | } 32 | } 33 | 34 | @Composable 35 | private fun getTimePickerDialog( 36 | selectedDate: CalendarInformation, 37 | listener: TimePickerDialog.OnTimeSetListener 38 | ): TimePickerDialog { 39 | val context = LocalContext.current 40 | val (hour, minute) = selectedDate.dateInformation 41 | return TimePickerDialog( 42 | context, 43 | listener, 44 | hour, 45 | minute, 46 | false 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/model/CalendarInformation.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.addmedication.model 2 | 3 | import androidx.compose.runtime.saveable.Saver 4 | import java.text.SimpleDateFormat 5 | import java.util.Calendar 6 | import java.util.Locale 7 | import java.util.MissingFormatArgumentException 8 | 9 | class CalendarInformation(private val calendar: Calendar) { 10 | 11 | val dateInformation = TimeInformation( 12 | hour = calendar.get(Calendar.HOUR_OF_DAY), 13 | minute = calendar.get(Calendar.MINUTE) 14 | ) 15 | 16 | fun getTimeInMillis() = calendar.timeInMillis 17 | 18 | fun getDateFormatted(pattern: String): String { 19 | return try { 20 | SimpleDateFormat(pattern, Locale.getDefault()).format(calendar.time) 21 | } catch (ex: MissingFormatArgumentException) { 22 | throw ex 23 | } 24 | } 25 | 26 | inner class TimeInformation( 27 | val hour: Int, 28 | val minute: Int, 29 | ) { 30 | operator fun component1(): Int = hour 31 | operator fun component2(): Int = minute 32 | } 33 | 34 | companion object { 35 | fun getStateSaver() = Saver( 36 | save = { state -> 37 | state.calendar 38 | }, 39 | restore = { 40 | CalendarInformation(it) 41 | } 42 | ) 43 | 44 | fun getStateListSaver() = Saver, MutableList>( 45 | save = { state -> 46 | state.map { it.calendar }.toMutableList() 47 | }, 48 | restore = { 49 | it.map { CalendarInformation(it) }.toMutableList() 50 | } 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/navigation/AddMedicationDestination.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.addmedication.navigation 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.runtime.MutableState 5 | import androidx.navigation.NavController 6 | import androidx.navigation.NavGraphBuilder 7 | import androidx.navigation.compose.composable 8 | import com.waseefakhtar.doseapp.core.navigation.DoseNavigationDestination 9 | import com.waseefakhtar.doseapp.domain.model.Medication 10 | import com.waseefakhtar.doseapp.feature.addmedication.AddMedicationRoute 11 | import com.waseefakhtar.doseapp.feature.home.navigation.ASK_ALARM_PERMISSION 12 | import com.waseefakhtar.doseapp.feature.home.navigation.ASK_NOTIFICATION_PERMISSION 13 | 14 | object AddMedicationDestination : DoseNavigationDestination { 15 | override val route = "add_medication_route" 16 | override val destination = "add_medication_destination" 17 | } 18 | 19 | fun NavGraphBuilder.addMedicationGraph(navController: NavController, bottomBarVisibility: MutableState, fabVisibility: MutableState, onBackClicked: () -> Unit, navigateToMedicationConfirm: (List) -> Unit) { 20 | composable(route = AddMedicationDestination.route) { 21 | LaunchedEffect(null) { 22 | bottomBarVisibility.value = false 23 | fabVisibility.value = false 24 | } 25 | 26 | navController.previousBackStackEntry?.savedStateHandle.apply { 27 | this?.set(ASK_NOTIFICATION_PERMISSION, true) 28 | } 29 | navController.previousBackStackEntry?.savedStateHandle.apply { 30 | this?.set(ASK_ALARM_PERMISSION, true) 31 | } 32 | AddMedicationRoute(onBackClicked, navigateToMedicationConfirm) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.addmedication.viewmodel 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.ViewModel 5 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 6 | import com.waseefakhtar.doseapp.domain.model.Medication 7 | import com.waseefakhtar.doseapp.feature.addmedication.model.CalendarInformation 8 | import com.waseefakhtar.doseapp.util.Frequency 9 | import com.waseefakhtar.doseapp.util.MedicationType 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import java.util.Calendar 13 | import java.util.Date 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class AddMedicationViewModel @Inject constructor( 18 | private val analyticsHelper: AnalyticsHelper, 19 | @ApplicationContext private val context: Context 20 | ) : ViewModel() { 21 | 22 | fun createMedications( 23 | name: String, 24 | dosage: Int, 25 | frequency: String, 26 | startDate: Date, 27 | endDate: Date, 28 | medicationTimes: List, 29 | type: MedicationType, 30 | ): List { 31 | val frequencyValue = Frequency.valueOf(frequency) 32 | val interval = try { 33 | frequencyValue.days 34 | } catch (e: IllegalArgumentException) { 35 | throw IllegalArgumentException("Invalid frequency: $frequency") 36 | } 37 | 38 | val oneDayInMillis = 86400 * 1000 // Number of milliseconds in one day 39 | val durationInDays = ((endDate.time + oneDayInMillis - startDate.time) / oneDayInMillis).toInt() 40 | 41 | // Always create at least one occurrence if we have a valid duration 42 | val numOccurrences = if (durationInDays > 0) maxOf(1, durationInDays / interval) else 0 43 | 44 | // Create a Medication object for each occurrence and add it to a list 45 | val medications = mutableListOf() 46 | val calendar = Calendar.getInstance() 47 | calendar.time = startDate 48 | 49 | val formattedFrequency = context.getString(frequencyValue.stringResId, frequencyValue.days) 50 | for (i in 0 until numOccurrences) { 51 | for (medicationTime in medicationTimes) { 52 | val medication = Medication( 53 | id = 0, 54 | name = name, 55 | dosage = dosage, 56 | frequency = formattedFrequency, 57 | startDate = startDate, 58 | endDate = endDate, 59 | medicationTaken = false, 60 | medicationTime = getMedicationTime(medicationTime, calendar), 61 | type = type 62 | ) 63 | medications.add(medication) 64 | } 65 | 66 | // Increment the date based on the frequency interval 67 | calendar.add(Calendar.DAY_OF_YEAR, interval) 68 | } 69 | 70 | return medications 71 | } 72 | 73 | private fun getMedicationTime(medicationTime: CalendarInformation, calendar: Calendar): Date { 74 | calendar.set(Calendar.HOUR_OF_DAY, medicationTime.dateInformation.hour) 75 | calendar.set(Calendar.MINUTE, medicationTime.dateInformation.minute) 76 | return calendar.time 77 | } 78 | 79 | fun logEvent(eventName: String) { 80 | analyticsHelper.logEvent(eventName = eventName) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/calendar/CalendarScreen.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.calendar 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.text.font.FontWeight 8 | 9 | @Composable 10 | fun CalendarRoute( 11 | modifier: Modifier = Modifier, 12 | // viewModel: CalendarViewModel = hiltViewModel() 13 | ) { 14 | CalendarScreen() 15 | } 16 | 17 | @Composable 18 | fun CalendarScreen() { 19 | Text( 20 | text = "Coming Soon \uD83D\uDEA7", 21 | fontWeight = FontWeight.Bold, 22 | style = MaterialTheme.typography.displaySmall 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/calendar/navigation/CalendarNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.calendar.navigation 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.runtime.MutableState 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.compose.composable 7 | import com.waseefakhtar.doseapp.core.navigation.DoseNavigationDestination 8 | import com.waseefakhtar.doseapp.feature.calendar.CalendarRoute 9 | 10 | object CalendarDestination : DoseNavigationDestination { 11 | override val route = "calendar_route" 12 | override val destination = "calendar_destination" 13 | } 14 | 15 | fun NavGraphBuilder.calendarGraph(bottomBarVisibility: MutableState, fabVisibility: MutableState) { 16 | composable(route = CalendarDestination.route) { 17 | LaunchedEffect(null) { 18 | bottomBarVisibility.value = true 19 | fabVisibility.value = false 20 | } 21 | CalendarRoute() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/calendar/viewmodel/CalendarState.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.calendar.viewmodel 2 | 3 | import java.util.Date 4 | 5 | // TODO: Fill out when Calendar feature is implemented 6 | data class CalendarState( 7 | val currentDate: Date? = null 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/calendar/viewmodel/CalendarViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.calendar.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import javax.inject.Inject 9 | 10 | @HiltViewModel 11 | class CalendarViewModel @Inject constructor() : ViewModel() { 12 | 13 | var state by mutableStateOf(CalendarState()) 14 | private set 15 | 16 | // TODO: Fill out when Calendar feature is implemented 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/history/HistoryNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.history 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.runtime.MutableState 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.compose.composable 7 | import com.waseefakhtar.doseapp.core.navigation.DoseNavigationDestination 8 | import com.waseefakhtar.doseapp.domain.model.Medication 9 | 10 | object HistoryDestination : DoseNavigationDestination { 11 | override val route = "history_route" 12 | override val destination = "history_destination" 13 | } 14 | 15 | fun NavGraphBuilder.historyGraph(bottomBarVisibility: MutableState, fabVisibility: MutableState, navigateToMedicationDetail: (Medication) -> Unit) { 16 | composable(route = HistoryDestination.route) { 17 | LaunchedEffect(null) { 18 | bottomBarVisibility.value = true 19 | fabVisibility.value = false 20 | } 21 | HistoryRoute(navigateToMedicationDetail) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/history/HistoryScreen.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.history 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Scaffold 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TopAppBar 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.unit.dp 23 | import androidx.hilt.navigation.compose.hiltViewModel 24 | import com.waseefakhtar.doseapp.R 25 | import com.waseefakhtar.doseapp.domain.model.Medication 26 | import com.waseefakhtar.doseapp.extension.hasPassed 27 | import com.waseefakhtar.doseapp.feature.history.viewmodel.HistoryState 28 | import com.waseefakhtar.doseapp.feature.history.viewmodel.HistoryViewModel 29 | import com.waseefakhtar.doseapp.feature.home.MedicationCard 30 | import com.waseefakhtar.doseapp.feature.home.MedicationListItem 31 | 32 | @Composable 33 | fun HistoryRoute( 34 | navigateToMedicationDetail: (Medication) -> Unit, 35 | viewModel: HistoryViewModel = hiltViewModel() 36 | ) { 37 | val state = viewModel.state 38 | HistoryScreen( 39 | state = state, 40 | navigateToMedicationDetail = navigateToMedicationDetail 41 | ) 42 | } 43 | 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | @Composable 46 | fun HistoryScreen( 47 | state: HistoryState, 48 | navigateToMedicationDetail: (Medication) -> Unit 49 | ) { 50 | Scaffold( 51 | topBar = { 52 | TopAppBar( 53 | modifier = Modifier 54 | .padding(top = 16.dp), 55 | title = { 56 | Text( 57 | text = stringResource(id = R.string.history), 58 | fontWeight = FontWeight.Bold, 59 | style = MaterialTheme.typography.displaySmall, 60 | ) 61 | } 62 | ) 63 | }, 64 | bottomBar = { }, 65 | ) { innerPadding -> 66 | Column( 67 | modifier = Modifier.padding(innerPadding), 68 | verticalArrangement = Arrangement.spacedBy(8.dp) 69 | ) { 70 | MedicationList( 71 | state = state, 72 | navigateToMedicationDetail = navigateToMedicationDetail 73 | ) 74 | } 75 | } 76 | } 77 | 78 | @Composable 79 | fun MedicationList( 80 | state: HistoryState, 81 | navigateToMedicationDetail: (Medication) -> Unit 82 | ) { 83 | 84 | val filteredMedicationList = state.medications.filter { it.medicationTime.hasPassed() } 85 | val sortedMedicationList: List = filteredMedicationList.sortedBy { it.medicationTime }.map { MedicationListItem.MedicationItem(it) } 86 | 87 | when (sortedMedicationList.isEmpty()) { 88 | true -> EmptyView() 89 | false -> MedicationLazyColumn(sortedMedicationList, navigateToMedicationDetail) 90 | } 91 | } 92 | 93 | @Composable 94 | fun MedicationLazyColumn(sortedMedicationList: List, navigateToMedicationDetail: (Medication) -> Unit) { 95 | LazyColumn( 96 | modifier = Modifier, 97 | contentPadding = PaddingValues(vertical = 8.dp) 98 | ) { 99 | items( 100 | items = sortedMedicationList, 101 | itemContent = { 102 | when (it) { 103 | is MedicationListItem.OverviewItem -> { } 104 | is MedicationListItem.HeaderItem -> { 105 | Text( 106 | modifier = Modifier 107 | .padding(4.dp, 12.dp, 8.dp, 0.dp) 108 | .fillMaxWidth(), 109 | text = it.headerText.uppercase(), 110 | textAlign = TextAlign.Center, 111 | style = MaterialTheme.typography.titleMedium, 112 | ) 113 | } 114 | is MedicationListItem.MedicationItem -> { 115 | MedicationCard( 116 | medication = it.medication, 117 | navigateToMedicationDetail = { medication -> 118 | navigateToMedicationDetail(medication) 119 | } 120 | ) 121 | } 122 | } 123 | } 124 | ) 125 | } 126 | } 127 | 128 | @Composable 129 | fun EmptyView() { 130 | Column( 131 | modifier = Modifier 132 | .fillMaxSize() 133 | .padding(16.dp), 134 | verticalArrangement = Arrangement.Center, 135 | horizontalAlignment = Alignment.CenterHorizontally 136 | ) { 137 | Text( 138 | modifier = Modifier.padding(16.dp), 139 | style = MaterialTheme.typography.headlineMedium, 140 | text = stringResource(id = R.string.no_history_yet), 141 | color = MaterialTheme.colorScheme.tertiary 142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/history/viewmodel/HistoryState.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.history.viewmodel 2 | 3 | import com.waseefakhtar.doseapp.domain.model.Medication 4 | 5 | data class HistoryState( 6 | val medications: List = emptyList() 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/history/viewmodel/HistoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.history.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.waseefakhtar.doseapp.feature.home.usecase.GetMedicationsUseCase 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.launchIn 11 | import kotlinx.coroutines.flow.onEach 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class HistoryViewModel @Inject constructor( 17 | private val getMedicationsUseCase: GetMedicationsUseCase 18 | ) : ViewModel() { 19 | 20 | var state by mutableStateOf(HistoryState()) 21 | private set 22 | 23 | init { 24 | loadMedications() 25 | } 26 | 27 | fun loadMedications() { 28 | viewModelScope.launch { 29 | getMedicationsUseCase.getMedications().onEach { medicationList -> 30 | state = state.copy( 31 | medications = medicationList 32 | ) 33 | }.launchIn(viewModelScope) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/home/MedicationCard.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.home 2 | 3 | import androidx.compose.foundation.border 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.aspectRatio 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.Card 14 | import androidx.compose.material3.CardDefaults 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.res.painterResource 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.dp 27 | import com.waseefakhtar.doseapp.R 28 | import com.waseefakhtar.doseapp.domain.model.Medication 29 | import com.waseefakhtar.doseapp.util.MedicationType 30 | import java.util.Date 31 | 32 | @Composable 33 | fun MedicationCard( 34 | medication: Medication, 35 | navigateToMedicationDetail: (Medication) -> Unit 36 | ) { 37 | val (cardColor, boxColor, textColor) = medication.type.getCardColor() 38 | 39 | Card( 40 | modifier = Modifier 41 | .fillMaxWidth() 42 | .padding(vertical = 8.dp), 43 | onClick = { navigateToMedicationDetail(medication) }, 44 | shape = RoundedCornerShape(30.dp), 45 | colors = CardDefaults.cardColors( 46 | containerColor = Color(cardColor), 47 | ) 48 | ) { 49 | Row( 50 | modifier = Modifier.padding(24.dp), 51 | verticalAlignment = Alignment.CenterVertically 52 | ) { 53 | Column( 54 | modifier = Modifier.weight(2f), 55 | horizontalAlignment = Alignment.Start 56 | ) { 57 | Text( 58 | text = medication.name, 59 | fontWeight = FontWeight.Bold, 60 | style = MaterialTheme.typography.titleLarge, 61 | color = Color(boxColor) 62 | ) 63 | 64 | val doseAndType = "${medication.dosage} ${ 65 | stringResource( 66 | when (medication.type) { 67 | MedicationType.TABLET -> R.string.tablet 68 | MedicationType.CAPSULE -> R.string.capsule 69 | MedicationType.SYRUP -> R.string.type_syrup 70 | MedicationType.DROPS -> R.string.drops 71 | MedicationType.SPRAY -> R.string.spray 72 | MedicationType.GEL -> R.string.gel 73 | } 74 | ).lowercase() 75 | }" 76 | 77 | Text( 78 | text = doseAndType, 79 | color = Color(boxColor) 80 | ) 81 | } 82 | 83 | Box( 84 | modifier = Modifier 85 | .height(64.dp) 86 | .aspectRatio(1f) 87 | .border( 88 | width = 1.5.dp, color = Color(boxColor), shape = RoundedCornerShape(16.dp) 89 | ), 90 | contentAlignment = Alignment.Center 91 | ) { 92 | Icon( 93 | painter = painterResource( 94 | when (medication.type) { 95 | MedicationType.TABLET -> R.drawable.ic_tablet 96 | MedicationType.CAPSULE -> R.drawable.ic_capsule 97 | MedicationType.SYRUP -> R.drawable.ic_syrup 98 | MedicationType.DROPS -> R.drawable.ic_drops 99 | MedicationType.SPRAY -> R.drawable.ic_spray 100 | MedicationType.GEL -> R.drawable.ic_gel 101 | } 102 | ), 103 | contentDescription = stringResource( 104 | when (medication.type) { 105 | MedicationType.TABLET -> R.string.tablet 106 | MedicationType.CAPSULE -> R.string.capsule 107 | MedicationType.SYRUP -> R.string.type_syrup 108 | MedicationType.DROPS -> R.string.drops 109 | MedicationType.SPRAY -> R.string.spray 110 | MedicationType.GEL -> R.string.gel 111 | } 112 | ), 113 | modifier = Modifier.size(42.dp), 114 | tint = Color(boxColor) 115 | ) 116 | } 117 | } 118 | } 119 | } 120 | 121 | @Preview 122 | @Composable 123 | private fun MedicationCardTakeNowPreview() { 124 | MedicationCard( 125 | Medication( 126 | id = 123L, 127 | name = "A big big name for a little medication I needs to take", 128 | dosage = 1, 129 | frequency = "2", 130 | startDate = Date(), 131 | endDate = Date(), 132 | medicationTime = Date(), 133 | medicationTaken = false 134 | ) 135 | ) { } 136 | } 137 | 138 | @Preview 139 | @Composable 140 | private fun MedicationCardTakenPreview() { 141 | MedicationCard( 142 | Medication( 143 | id = 123L, 144 | name = "A big big name for a little medication I needs to take", 145 | dosage = 1, 146 | frequency = "2", 147 | startDate = Date(), 148 | endDate = Date(), 149 | medicationTime = Date(), 150 | medicationTaken = true, 151 | type = MedicationType.TABLET 152 | ) 153 | ) { } 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/home/data/CalendarDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.home.data 2 | 3 | import com.waseefakhtar.doseapp.extension.toDate 4 | import com.waseefakhtar.doseapp.extension.toFormattedDateString 5 | import com.waseefakhtar.doseapp.feature.home.model.CalendarModel 6 | import java.util.Calendar 7 | import java.util.Date 8 | 9 | class CalendarDataSource { 10 | 11 | val today: Date 12 | get() { 13 | return Date() 14 | } 15 | 16 | fun getLastSelectedDate(dateString: String): Date { 17 | return dateString.toDate() ?: today 18 | } 19 | fun getData(startDate: Date = today, lastSelectedDate: Date): CalendarModel { 20 | val calendar = Calendar.getInstance() 21 | calendar.time = startDate 22 | 23 | calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY) 24 | val firstDayOfWeek = calendar.time 25 | 26 | calendar.add(Calendar.DAY_OF_YEAR, 6) 27 | val endDayOfWeek = calendar.time 28 | 29 | val visibleDates = getDatesBetween(firstDayOfWeek, endDayOfWeek) 30 | return toCalendarModel(visibleDates, lastSelectedDate) 31 | } 32 | 33 | private fun getDatesBetween(startDate: Date, endDate: Date): List { 34 | val dateList = mutableListOf() 35 | val calendar = Calendar.getInstance() 36 | calendar.time = startDate 37 | 38 | while (calendar.time <= endDate) { 39 | dateList.add(calendar.time) 40 | calendar.add(Calendar.DAY_OF_YEAR, 1) 41 | } 42 | return dateList 43 | } 44 | 45 | private fun toCalendarModel( 46 | dateList: List, 47 | lastSelectedDate: Date 48 | ): CalendarModel { 49 | return CalendarModel( 50 | selectedDate = toItemModel(lastSelectedDate, true), 51 | visibleDates = dateList.map { 52 | toItemModel( 53 | date = it, 54 | isSelectedDate = it.toFormattedDateString() == lastSelectedDate.toFormattedDateString() 55 | ) 56 | } 57 | ) 58 | } 59 | 60 | private fun toItemModel(date: Date, isSelectedDate: Boolean): CalendarModel.DateModel { 61 | return CalendarModel.DateModel( 62 | isSelected = isSelectedDate, 63 | isToday = isToday(date), 64 | date = date 65 | ) 66 | } 67 | 68 | private fun isToday(date: Date): Boolean { 69 | val todayDate = today 70 | return date.toFormattedDateString() == todayDate.toFormattedDateString() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/home/model/CalendarModel.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.home.model 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | import java.util.Locale 6 | 7 | data class CalendarModel( 8 | val selectedDate: DateModel, // the date selected by the User. by default is Today. 9 | val visibleDates: List // the dates shown on the screen 10 | ) { 11 | 12 | val startDate: DateModel = visibleDates.first() // the first of the visible dates 13 | val endDate: DateModel = visibleDates.last() // the last of the visible dates 14 | 15 | data class DateModel( 16 | val date: Date, 17 | val isSelected: Boolean, 18 | val isToday: Boolean 19 | ) { 20 | val day: String = SimpleDateFormat("E", Locale.getDefault()).format(date) ?: "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/home/navigation/HomeDestination.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.home.navigation 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.runtime.MutableState 5 | import androidx.navigation.NavController 6 | import androidx.navigation.NavGraphBuilder 7 | import androidx.navigation.compose.composable 8 | import com.waseefakhtar.doseapp.core.navigation.DoseNavigationDestination 9 | import com.waseefakhtar.doseapp.domain.model.Medication 10 | import com.waseefakhtar.doseapp.feature.home.HomeRoute 11 | 12 | const val ASK_NOTIFICATION_PERMISSION = "notification_permission" 13 | const val ASK_ALARM_PERMISSION = "alarm_permission" 14 | object HomeDestination : DoseNavigationDestination { 15 | override val route = "home_route" 16 | override val destination = "home_destination" 17 | } 18 | 19 | fun NavGraphBuilder.homeGraph(navController: NavController, bottomBarVisibility: MutableState, fabVisibility: MutableState, navigateToMedicationDetail: (Medication) -> Unit) { 20 | composable(route = HomeDestination.route) { 21 | LaunchedEffect(null) { 22 | bottomBarVisibility.value = true 23 | fabVisibility.value = true 24 | } 25 | val askNotificationPermission = navController.currentBackStackEntry?.savedStateHandle?.get(ASK_NOTIFICATION_PERMISSION) ?: false 26 | val askAlarmPermission = navController.currentBackStackEntry?.savedStateHandle?.get(ASK_ALARM_PERMISSION) ?: false 27 | 28 | HomeRoute(navController, askNotificationPermission, askAlarmPermission, navigateToMedicationDetail) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/home/usecase/GetMedicationsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.home.usecase 2 | 3 | import com.waseefakhtar.doseapp.domain.model.Medication 4 | import com.waseefakhtar.doseapp.domain.repository.MedicationRepository 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | class GetMedicationsUseCase @Inject constructor( 9 | private val repository: MedicationRepository 10 | ) { 11 | 12 | fun getMedications(date: String? = null): Flow> { 13 | return if (date != null) { 14 | repository.getMedicationsForDate(date) 15 | } else repository.getAllMedications() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/home/usecase/UpdateMedicationUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.home.usecase 2 | 3 | import com.waseefakhtar.doseapp.domain.model.Medication 4 | import com.waseefakhtar.doseapp.domain.repository.MedicationRepository 5 | import javax.inject.Inject 6 | 7 | class UpdateMedicationUseCase @Inject constructor( 8 | private val repository: MedicationRepository 9 | ) { 10 | 11 | suspend fun updateMedication(medication: Medication) { 12 | return repository.updateMedication(medication) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/home/viewmodel/HomeState.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.home.viewmodel 2 | 3 | import com.waseefakhtar.doseapp.domain.model.Medication 4 | 5 | data class HomeState( 6 | val greeting: String = "", 7 | val userName: String = "", 8 | val lastSelectedDate: String, 9 | val medications: List = emptyList() 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/home/viewmodel/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.home.viewmodel 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 7 | import com.waseefakhtar.doseapp.domain.model.Medication 8 | import com.waseefakhtar.doseapp.extension.toFormattedYearMonthDateString 9 | import com.waseefakhtar.doseapp.feature.home.model.CalendarModel 10 | import com.waseefakhtar.doseapp.feature.home.usecase.GetMedicationsUseCase 11 | import com.waseefakhtar.doseapp.feature.home.usecase.UpdateMedicationUseCase 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.flow.SharingStarted 16 | import kotlinx.coroutines.flow.combine 17 | import kotlinx.coroutines.flow.flatMapLatest 18 | import kotlinx.coroutines.flow.stateIn 19 | import kotlinx.coroutines.launch 20 | import java.util.Date 21 | import javax.inject.Inject 22 | 23 | @HiltViewModel 24 | class HomeViewModel @Inject constructor( 25 | private val getMedicationsUseCase: GetMedicationsUseCase, 26 | private val updateMedicationUseCase: UpdateMedicationUseCase, 27 | private val savedStateHandle: SavedStateHandle, 28 | private val analyticsHelper: AnalyticsHelper 29 | ) : ViewModel() { 30 | 31 | private val _selectedDate = MutableStateFlow(Date()) 32 | private val _dateFilter = savedStateHandle.getStateFlow( 33 | DATE_FILTER_KEY, 34 | Date().toFormattedYearMonthDateString() 35 | ) 36 | private val _greeting = MutableStateFlow("") 37 | private val _userName = MutableStateFlow("") 38 | 39 | @OptIn(ExperimentalCoroutinesApi::class) 40 | private val _medications = _dateFilter.flatMapLatest { selectedDate -> 41 | getMedicationsUseCase.getMedications(selectedDate) 42 | } 43 | 44 | val homeUiState = combine( 45 | _selectedDate, 46 | _medications, 47 | _dateFilter, 48 | _greeting, 49 | _userName 50 | ) { selectedDate, medications, dateFilter, greeting, userName -> 51 | HomeState( 52 | lastSelectedDate = dateFilter, 53 | medications = medications.sortedBy { it.medicationTime }, 54 | greeting = greeting, 55 | userName = userName 56 | ) 57 | }.stateIn( 58 | viewModelScope, 59 | SharingStarted.WhileSubscribed(5000), 60 | HomeState(lastSelectedDate = Date().toFormattedYearMonthDateString()) 61 | ) 62 | 63 | fun updateSelectedDate(date: Date) { 64 | _selectedDate.value = date 65 | // Update the date filter to trigger new medication fetch 66 | savedStateHandle[DATE_FILTER_KEY] = date.toFormattedYearMonthDateString() 67 | } 68 | 69 | init { 70 | getUserName() 71 | getGreeting() 72 | } 73 | 74 | private fun getUserName() { 75 | _userName.value = "Kathryn" 76 | // TODO: Get user name from DB 77 | } 78 | 79 | private fun getGreeting() { 80 | _greeting.value = "Greeting" 81 | // TODO: Get greeting by checking system time 82 | } 83 | 84 | fun selectDate(selectedDate: CalendarModel.DateModel) { 85 | savedStateHandle[DATE_FILTER_KEY] = selectedDate.date.toFormattedYearMonthDateString() 86 | } 87 | 88 | fun takeMedication(medication: Medication) { 89 | viewModelScope.launch { 90 | updateMedicationUseCase.updateMedication(medication) 91 | } 92 | } 93 | 94 | fun getUserPlan() { 95 | // TODO: Get user plan 96 | } 97 | 98 | fun logEvent(eventName: String) { 99 | analyticsHelper.logEvent(eventName = eventName) 100 | } 101 | 102 | companion object { 103 | const val DATE_FILTER_KEY = "medication_date_filter" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/MedicationConfirmRoute.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.medicationconfirm 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.ArrowBack 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.FloatingActionButton 13 | import androidx.compose.material3.FloatingActionButtonDefaults 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.res.pluralStringResource 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.unit.dp 26 | import androidx.hilt.navigation.compose.hiltViewModel 27 | import com.google.firebase.crashlytics.FirebaseCrashlytics 28 | import com.waseefakhtar.doseapp.R 29 | import com.waseefakhtar.doseapp.analytics.AnalyticsEvents 30 | import com.waseefakhtar.doseapp.domain.model.Medication 31 | import com.waseefakhtar.doseapp.extension.toFormattedDateString 32 | import com.waseefakhtar.doseapp.feature.medicationconfirm.viewmodel.MedicationConfirmState 33 | import com.waseefakhtar.doseapp.feature.medicationconfirm.viewmodel.MedicationConfirmViewModel 34 | import com.waseefakhtar.doseapp.util.SnackbarUtil.Companion.showSnackbar 35 | 36 | @Composable 37 | fun MedicationConfirmRoute( 38 | medication: List?, 39 | onBackClicked: () -> Unit, 40 | navigateToHome: () -> Unit, 41 | modifier: Modifier = Modifier, 42 | viewModel: MedicationConfirmViewModel = hiltViewModel() 43 | ) { 44 | medication?.let { 45 | MedicationConfirmScreen( 46 | medications = it, 47 | viewModel = viewModel, 48 | onBackClicked = onBackClicked, 49 | navigateToHome = navigateToHome, 50 | logEvent = viewModel::logEvent 51 | ) 52 | } ?: { 53 | FirebaseCrashlytics.getInstance().log("Error: Cannot show MedicationConfirmScreen. Medication is null.") 54 | } 55 | } 56 | 57 | @Composable 58 | fun MedicationConfirmScreen( 59 | medications: List, 60 | viewModel: MedicationConfirmViewModel, 61 | logEvent: (String) -> Unit, 62 | onBackClicked: () -> Unit, 63 | navigateToHome: () -> Unit, 64 | ) { 65 | 66 | val context = LocalContext.current 67 | LaunchedEffect(Unit) { 68 | viewModel 69 | .isMedicationSaved 70 | .collect { 71 | showSnackbar( 72 | context.getString( 73 | R.string.medication_timely_reminders_setup_message, 74 | medications.first().name 75 | ) 76 | ) 77 | navigateToHome() 78 | logEvent.invoke(AnalyticsEvents.MEDICATIONS_SAVED) 79 | } 80 | } 81 | 82 | Column( 83 | modifier = Modifier.padding(0.dp, 16.dp), 84 | verticalArrangement = Arrangement.spacedBy(8.dp) 85 | ) { 86 | FloatingActionButton( 87 | onClick = { 88 | logEvent.invoke(AnalyticsEvents.MEDICATION_CONFIRM_ON_BACK_CLICKED) 89 | onBackClicked() 90 | }, 91 | elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp) 92 | ) { 93 | Icon( 94 | imageVector = Icons.Default.ArrowBack, 95 | contentDescription = stringResource(id = R.string.back) 96 | ) 97 | } 98 | } 99 | 100 | Column( 101 | modifier = Modifier.fillMaxSize(), 102 | verticalArrangement = Arrangement.Center 103 | ) { 104 | Text( 105 | text = stringResource(id = R.string.all_done), 106 | fontWeight = FontWeight.Bold, 107 | style = MaterialTheme.typography.displaySmall 108 | ) 109 | 110 | val medication = medications.first() 111 | Text( 112 | text = pluralStringResource( 113 | id = R.plurals.all_set, 114 | count = medications.size, 115 | medication.name, 116 | medications.size, 117 | medication.frequency.lowercase(), 118 | medication.endDate.toFormattedDateString() 119 | ), 120 | style = MaterialTheme.typography.titleMedium 121 | ) 122 | } 123 | 124 | Column( 125 | modifier = Modifier 126 | .padding(0.dp, 16.dp) 127 | .fillMaxSize(), 128 | verticalArrangement = Arrangement.Bottom 129 | ) { 130 | 131 | Button( 132 | modifier = Modifier 133 | .fillMaxWidth() 134 | .height(56.dp) 135 | .align(Alignment.CenterHorizontally), 136 | onClick = { 137 | logEvent.invoke(AnalyticsEvents.MEDICATION_CONFIRM_ON_CONFIRM_CLICKED) 138 | viewModel.addMedication(MedicationConfirmState(medications)) 139 | }, 140 | shape = MaterialTheme.shapes.extraLarge 141 | ) { 142 | Text( 143 | text = stringResource(id = R.string.confirm), 144 | style = MaterialTheme.typography.bodyLarge 145 | ) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/navigation/MedicationConfirmDestination.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.medicationconfirm.navigation 2 | 3 | import android.os.Bundle 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.MutableState 6 | import androidx.navigation.NavController 7 | import androidx.navigation.NavGraphBuilder 8 | import androidx.navigation.compose.composable 9 | import com.waseefakhtar.doseapp.core.navigation.DoseNavigationDestination 10 | import com.waseefakhtar.doseapp.domain.model.Medication 11 | import com.waseefakhtar.doseapp.feature.medicationconfirm.MedicationConfirmRoute 12 | 13 | const val MEDICATION = "medication" 14 | 15 | object MedicationConfirmDestination : DoseNavigationDestination { 16 | override val route = "medication_confirm_route" 17 | override val destination = "medication_confirm_destination" 18 | } 19 | 20 | fun NavGraphBuilder.medicationConfirmGraph(navController: NavController, bottomBarVisibility: MutableState, fabVisibility: MutableState, onBackClicked: () -> Unit, navigateToHome: () -> Unit) { 21 | 22 | composable( 23 | route = MedicationConfirmDestination.route, 24 | ) { 25 | LaunchedEffect(null) { 26 | bottomBarVisibility.value = false 27 | fabVisibility.value = false 28 | } 29 | val medicationBundle = navController.previousBackStackEntry?.savedStateHandle?.get(MEDICATION) 30 | val medicationList = medicationBundle?.getParcelableArrayList(MEDICATION) 31 | MedicationConfirmRoute(medicationList, onBackClicked, navigateToHome) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/usecase/AddMedicationUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.medicationconfirm.usecase 2 | 3 | import com.waseefakhtar.doseapp.domain.model.Medication 4 | import com.waseefakhtar.doseapp.domain.repository.MedicationRepository 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | class AddMedicationUseCase @Inject constructor( 9 | private val repository: MedicationRepository 10 | ) { 11 | suspend fun addMedication(medications: List): Flow> = repository.insertMedications(medications) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/viewmodel/MedicationConfirmState.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.medicationconfirm.viewmodel 2 | 3 | import com.waseefakhtar.doseapp.domain.model.Medication 4 | 5 | data class MedicationConfirmState( 6 | val medications: List 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/viewmodel/MedicationConfirmViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.medicationconfirm.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.waseefakhtar.doseapp.MedicationNotificationService 6 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 7 | import com.waseefakhtar.doseapp.feature.medicationconfirm.usecase.AddMedicationUseCase 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.MutableSharedFlow 10 | import kotlinx.coroutines.flow.asSharedFlow 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class MedicationConfirmViewModel @Inject constructor( 16 | private val addMedicationUseCase: AddMedicationUseCase, 17 | private val medicationNotificationService: MedicationNotificationService, 18 | private val analyticsHelper: AnalyticsHelper 19 | ) : ViewModel() { 20 | private val _isMedicationSaved = MutableSharedFlow() 21 | val isMedicationSaved = _isMedicationSaved.asSharedFlow() 22 | 23 | fun addMedication(state: MedicationConfirmState) { 24 | viewModelScope.launch { 25 | val medications = state.medications 26 | addMedicationUseCase.addMedication(medications).collect { savedMedications -> 27 | // Schedule notifications for saved medications that have proper IDs 28 | savedMedications.forEach { medication -> 29 | medicationNotificationService.scheduleNotification( 30 | medication = medication, 31 | analyticsHelper = analyticsHelper 32 | ) 33 | } 34 | _isMedicationSaved.emit(Unit) 35 | } 36 | } 37 | } 38 | 39 | fun logEvent(eventName: String) { 40 | analyticsHelper.logEvent(eventName = eventName) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/medicationdetail/MedicationDetailDestination.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.medicationdetail 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.runtime.MutableState 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavType 7 | import androidx.navigation.compose.composable 8 | import androidx.navigation.navArgument 9 | import androidx.navigation.navDeepLink 10 | import com.waseefakhtar.doseapp.core.navigation.DoseNavigationDestination 11 | import com.waseefakhtar.doseapp.core.navigation.NavigationConstants.DEEP_LINK_URI_PATTERN 12 | import com.waseefakhtar.doseapp.core.navigation.NavigationConstants.MEDICATION_ID 13 | 14 | object MedicationDetailDestination : DoseNavigationDestination { 15 | override val route = "medication_detail_route/{$MEDICATION_ID}" 16 | override val destination = "medication_detail_destination" 17 | 18 | fun createNavigationRoute(medicationId: Long) = "medication_detail_route/$medicationId" 19 | } 20 | 21 | fun NavGraphBuilder.medicationDetailGraph( 22 | bottomBarVisibility: MutableState, 23 | fabVisibility: MutableState, 24 | onBackClicked: () -> Unit 25 | ) { 26 | composable( 27 | route = MedicationDetailDestination.route, 28 | arguments = listOf( 29 | navArgument(MEDICATION_ID) { type = NavType.LongType } 30 | ), 31 | deepLinks = listOf( 32 | navDeepLink { 33 | uriPattern = DEEP_LINK_URI_PATTERN 34 | } 35 | ) 36 | ) { backStackEntry -> 37 | LaunchedEffect(null) { 38 | bottomBarVisibility.value = false 39 | fabVisibility.value = false 40 | } 41 | 42 | val medicationId = backStackEntry.arguments?.getLong(MEDICATION_ID) 43 | 44 | MedicationDetailRoute( 45 | medicationId = medicationId, 46 | onBackClicked = onBackClicked 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/medicationdetail/usecase/GetMedicationUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.medicationdetail.usecase 2 | 3 | import com.waseefakhtar.doseapp.domain.model.Medication 4 | import com.waseefakhtar.doseapp.domain.repository.MedicationRepository 5 | import javax.inject.Inject 6 | 7 | class GetMedicationUseCase @Inject constructor( 8 | private val repository: MedicationRepository 9 | ) { 10 | suspend operator fun invoke(id: Long): Medication? = repository.getMedicationById(id) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/feature/medicationdetail/viewmodel/MedicationDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.feature.medicationdetail.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.waseefakhtar.doseapp.analytics.AnalyticsHelper 6 | import com.waseefakhtar.doseapp.domain.model.Medication 7 | import com.waseefakhtar.doseapp.feature.home.usecase.UpdateMedicationUseCase 8 | import com.waseefakhtar.doseapp.feature.medicationdetail.usecase.GetMedicationUseCase 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class MedicationDetailViewModel @Inject constructor( 17 | private val getMedicationUseCase: GetMedicationUseCase, 18 | private val updateMedicationUseCase: UpdateMedicationUseCase, 19 | private val analyticsHelper: AnalyticsHelper 20 | ) : ViewModel() { 21 | private val _medication = MutableStateFlow(null) 22 | val medication = _medication.asStateFlow() 23 | 24 | fun getMedicationById(id: Long) { 25 | viewModelScope.launch { 26 | _medication.value = getMedicationUseCase(id) 27 | } 28 | } 29 | 30 | fun updateMedication(medication: Medication, isMedicationTaken: Boolean) { 31 | viewModelScope.launch { 32 | updateMedicationUseCase.updateMedication(medication.copy(medicationTaken = isMedicationTaken)) 33 | } 34 | } 35 | 36 | fun logEvent(eventName: String) { 37 | analyticsHelper.logEvent(eventName = eventName) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/navigation/DoseNavHost.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.navigation 2 | 3 | import android.os.Bundle 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.MutableState 6 | import androidx.compose.ui.Modifier 7 | import androidx.navigation.NavHostController 8 | import androidx.navigation.compose.NavHost 9 | import androidx.navigation.compose.rememberNavController 10 | import com.waseefakhtar.doseapp.feature.addmedication.navigation.addMedicationGraph 11 | import com.waseefakhtar.doseapp.feature.calendar.navigation.calendarGraph 12 | import com.waseefakhtar.doseapp.feature.history.historyGraph 13 | import com.waseefakhtar.doseapp.feature.home.navigation.HomeDestination 14 | import com.waseefakhtar.doseapp.feature.home.navigation.homeGraph 15 | import com.waseefakhtar.doseapp.feature.medicationconfirm.navigation.MEDICATION 16 | import com.waseefakhtar.doseapp.feature.medicationconfirm.navigation.MedicationConfirmDestination 17 | import com.waseefakhtar.doseapp.feature.medicationconfirm.navigation.medicationConfirmGraph 18 | import com.waseefakhtar.doseapp.feature.medicationdetail.MedicationDetailDestination 19 | import com.waseefakhtar.doseapp.feature.medicationdetail.medicationDetailGraph 20 | import com.waseefakhtar.doseapp.util.navigateSingleTop 21 | 22 | @Composable 23 | fun DoseNavHost( 24 | bottomBarVisibility: MutableState, 25 | fabVisibility: MutableState, 26 | modifier: Modifier = Modifier, 27 | navController: NavHostController = rememberNavController(), 28 | startDestination: String = HomeDestination.route 29 | ) { 30 | NavHost( 31 | navController = navController, 32 | startDestination = startDestination, 33 | modifier = modifier, 34 | ) { 35 | homeGraph( 36 | navController = navController, 37 | bottomBarVisibility = bottomBarVisibility, 38 | fabVisibility = fabVisibility, 39 | navigateToMedicationDetail = { medication -> 40 | navController.navigate( 41 | MedicationDetailDestination.createNavigationRoute(medication.id) 42 | ) 43 | } 44 | ) 45 | historyGraph( 46 | bottomBarVisibility = bottomBarVisibility, 47 | fabVisibility = fabVisibility, 48 | navigateToMedicationDetail = { medication -> 49 | navController.navigate( 50 | MedicationDetailDestination.createNavigationRoute(medication.id) 51 | ) 52 | } 53 | ) 54 | medicationDetailGraph( 55 | bottomBarVisibility = bottomBarVisibility, 56 | fabVisibility = fabVisibility, 57 | onBackClicked = { navController.navigateUp() } 58 | ) 59 | calendarGraph(bottomBarVisibility, fabVisibility) 60 | addMedicationGraph( 61 | navController = navController, 62 | bottomBarVisibility = bottomBarVisibility, 63 | fabVisibility = fabVisibility, 64 | onBackClicked = { navController.navigateUp() }, 65 | navigateToMedicationConfirm = { 66 | // TODO: Replace with medication id 67 | val bundle = Bundle() 68 | bundle.putParcelableArrayList(MEDICATION, ArrayList(it)) 69 | navController.currentBackStackEntry?.savedStateHandle.apply { 70 | this?.set(MEDICATION, bundle) 71 | } 72 | navController.navigate(MedicationConfirmDestination.route) 73 | } 74 | ) 75 | medicationConfirmGraph( 76 | navController = navController, 77 | bottomBarVisibility = bottomBarVisibility, 78 | fabVisibility = fabVisibility, 79 | onBackClicked = { navController.navigateUp() }, 80 | navigateToHome = { 81 | navController.navigateSingleTop(HomeDestination.route) 82 | } 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/navigation/DoseTopLevelNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.navigation 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.DateRange 5 | import androidx.compose.material.icons.filled.Home 6 | import androidx.compose.material.icons.outlined.DateRange 7 | import androidx.compose.material.icons.outlined.Home 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.navigation.NavGraph.Companion.findStartDestination 10 | import androidx.navigation.NavHostController 11 | import com.waseefakhtar.doseapp.R 12 | import com.waseefakhtar.doseapp.feature.history.HistoryDestination 13 | import com.waseefakhtar.doseapp.feature.home.navigation.HomeDestination 14 | 15 | class DoseTopLevelNavigation(private val navController: NavHostController) { 16 | 17 | fun navigateTo(destination: TopLevelDestination) { 18 | navController.navigate(destination.route) { 19 | // Pop up to the start destination of the graph to 20 | // avoid building up a large stack of destinations 21 | // on the back stack as users select items 22 | popUpTo(navController.graph.findStartDestination().id) { 23 | saveState = true 24 | } 25 | // Avoid multiple copies of the same destination when 26 | // reselecting the same item 27 | launchSingleTop = true 28 | // Restore state when reselecting a previously selected item 29 | restoreState = true 30 | } 31 | } 32 | } 33 | 34 | data class TopLevelDestination( 35 | val route: String, 36 | val selectedIcon: ImageVector, 37 | val unselectedIcon: ImageVector, 38 | val iconTextId: Int 39 | ) 40 | 41 | val TOP_LEVEL_DESTINATIONS = listOf( 42 | TopLevelDestination( 43 | route = HomeDestination.route, 44 | selectedIcon = Icons.Filled.Home, 45 | unselectedIcon = Icons.Outlined.Home, 46 | iconTextId = R.string.home 47 | ), 48 | TopLevelDestination( 49 | route = HistoryDestination.route, 50 | selectedIcon = Icons.Filled.DateRange, 51 | unselectedIcon = Icons.Outlined.DateRange, 52 | iconTextId = R.string.history 53 | ), 54 | ) 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.ViewCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun DoseAppTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | darkTheme -> DarkColorScheme 53 | else -> LightColorScheme 54 | } 55 | val view = LocalView.current 56 | if (!view.isInEditMode) { 57 | SideEffect { 58 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 59 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 60 | } 61 | } 62 | 63 | MaterialTheme( 64 | colorScheme = colorScheme, 65 | typography = Typography, 66 | content = content 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/util/DurationFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.res.pluralStringResource 5 | import com.waseefakhtar.doseapp.extension.Duration 6 | 7 | @Composable 8 | fun formatDurationText(duration: Duration): String { 9 | val primary = 10 | pluralStringResource( 11 | duration.primaryType.pluralResId, 12 | duration.primary, 13 | duration.primary, 14 | ) 15 | 16 | return if (duration.remainder != null && duration.remainderType != null) { 17 | val remainder = 18 | pluralStringResource( 19 | duration.remainderType.pluralResId, 20 | duration.remainder, 21 | duration.remainder, 22 | ) 23 | "$primary, $remainder" 24 | } else { 25 | primary 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/util/FrequencyUtil.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.util 2 | 3 | import com.waseefakhtar.doseapp.R 4 | 5 | enum class Frequency(val stringResId: Int, val days: Int) { 6 | EVERYDAY(R.string.everyday, 1), 7 | EVERY_2_DAYS(R.string.every_n_days, 2), 8 | EVERY_3_DAYS(R.string.every_n_days, 3), 9 | EVERY_4_DAYS(R.string.every_n_days, 4), 10 | EVERY_5_DAYS(R.string.every_n_days, 5), 11 | EVERY_6_DAYS(R.string.every_n_days, 6), 12 | EVERY_WEEK(R.string.every_week, 7), 13 | EVERY_2_WEEKS(R.string.every_n_weeks, 14), 14 | EVERY_3_WEEKS(R.string.every_n_weeks, 21), 15 | EVERY_MONTH(R.string.every_month, 30); 16 | 17 | companion object { 18 | fun fromDays(days: Int): Frequency = when (days) { 19 | 1 -> EVERYDAY 20 | 2 -> EVERY_2_DAYS 21 | 3 -> EVERY_3_DAYS 22 | 4 -> EVERY_4_DAYS 23 | 5 -> EVERY_5_DAYS 24 | 6 -> EVERY_6_DAYS 25 | 7 -> EVERY_WEEK 26 | 14 -> EVERY_2_WEEKS 27 | 21 -> EVERY_3_WEEKS 28 | 30 -> EVERY_MONTH 29 | else -> EVERYDAY 30 | } 31 | } 32 | } 33 | 34 | fun getFrequencyList(): List = listOf( 35 | Frequency.EVERYDAY, 36 | Frequency.EVERY_2_DAYS, 37 | Frequency.EVERY_3_DAYS, 38 | Frequency.EVERY_4_DAYS, 39 | Frequency.EVERY_5_DAYS, 40 | Frequency.EVERY_6_DAYS, 41 | Frequency.EVERY_WEEK, 42 | Frequency.EVERY_2_WEEKS, 43 | Frequency.EVERY_3_WEEKS, 44 | Frequency.EVERY_MONTH 45 | ) 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/util/MedicationType.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.util 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | enum class MedicationType { 7 | TABLET, 8 | CAPSULE, 9 | SYRUP, 10 | DROPS, 11 | SPRAY, 12 | GEL; 13 | 14 | companion object { 15 | fun getDefault() = TABLET 16 | } 17 | 18 | @Composable 19 | fun getCardColor() = when (this) { 20 | TABLET -> Triple( 21 | MaterialTheme.colorScheme.primaryContainer.value, 22 | MaterialTheme.colorScheme.primary.value, 23 | MaterialTheme.colorScheme.onPrimaryContainer.value 24 | ) 25 | CAPSULE -> Triple( 26 | MaterialTheme.colorScheme.secondaryContainer.value, 27 | MaterialTheme.colorScheme.secondary.value, 28 | MaterialTheme.colorScheme.onSecondaryContainer.value 29 | ) 30 | SYRUP -> Triple( 31 | MaterialTheme.colorScheme.tertiaryContainer.value, 32 | MaterialTheme.colorScheme.tertiary.value, 33 | MaterialTheme.colorScheme.onTertiaryContainer.value 34 | ) 35 | DROPS -> Triple( 36 | MaterialTheme.colorScheme.surfaceVariant.value, 37 | MaterialTheme.colorScheme.primary.value, 38 | MaterialTheme.colorScheme.onSurfaceVariant.value 39 | ) 40 | SPRAY -> Triple( 41 | MaterialTheme.colorScheme.errorContainer.value, 42 | MaterialTheme.colorScheme.error.value, 43 | MaterialTheme.colorScheme.onErrorContainer.value 44 | ) 45 | GEL -> Triple( 46 | MaterialTheme.colorScheme.inversePrimary.value, 47 | MaterialTheme.colorScheme.primary.value, 48 | MaterialTheme.colorScheme.onPrimaryContainer.value 49 | ) 50 | } 51 | } 52 | 53 | fun getMedicationTypes(): List = MedicationType.values().toList() 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/util/SnackbarUtil.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.util 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.material3.Snackbar 6 | import androidx.compose.material3.SnackbarHost 7 | import androidx.compose.material3.SnackbarHostState 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.rememberCoroutineScope 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.zIndex 16 | import com.waseefakhtar.doseapp.ui.theme.Pink40 17 | import kotlinx.coroutines.launch 18 | 19 | class SnackbarUtil { 20 | 21 | companion object { 22 | private val snackbarMessage = mutableStateOf("") 23 | private var isSnackbarVisible = mutableStateOf(false) 24 | 25 | fun showSnackbar(message: String) { 26 | snackbarMessage.value = message 27 | isSnackbarVisible.value = true 28 | } 29 | 30 | fun getSnackbarMessage() = snackbarMessage 31 | 32 | fun hideSnackbar() { 33 | isSnackbarVisible.value = false 34 | } 35 | 36 | fun isSnackbarVisible() = isSnackbarVisible 37 | 38 | @Composable 39 | fun SnackbarWithoutScaffold( 40 | message: String, 41 | isVisible: Boolean, 42 | onVisibilityChange: (Boolean) -> Unit 43 | ) { 44 | val snackState = remember { SnackbarHostState() } 45 | val snackScope = rememberCoroutineScope() 46 | 47 | Box( 48 | modifier = Modifier 49 | .fillMaxWidth() 50 | .zIndex(10f), 51 | contentAlignment = Alignment.BottomCenter 52 | ) { 53 | SnackbarHost( 54 | modifier = Modifier, 55 | hostState = snackState 56 | ) { 57 | Snackbar( 58 | snackbarData = it, 59 | containerColor = Pink40, 60 | contentColor = androidx.compose.ui.graphics.Color.White 61 | ) 62 | } 63 | } 64 | 65 | if (isVisible) { 66 | LaunchedEffect(Unit) { 67 | snackScope.launch { 68 | snackState.showSnackbar(message) 69 | onVisibilityChange(false) 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/util/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.res.stringResource 5 | import com.waseefakhtar.doseapp.R 6 | import com.waseefakhtar.doseapp.domain.model.Medication 7 | import com.waseefakhtar.doseapp.extension.toFormattedDateString 8 | import java.util.Calendar 9 | import java.util.concurrent.TimeUnit 10 | import kotlin.math.abs 11 | 12 | const val HOUR_MINUTE_FORMAT = "h:mm a" 13 | @Composable 14 | fun getTimeRemaining(medication: Medication): String { 15 | val currentTime = Calendar.getInstance().time 16 | val dateBefore = medication.medicationTime 17 | val timeDiff = abs(currentTime.time - dateBefore.time) 18 | 19 | // If the medication is scheduled for a future date, display days remaining 20 | if (medication.medicationTime.toFormattedDateString() != medication.endDate.toFormattedDateString()) { 21 | val daysRemaining = TimeUnit.DAYS.convert(timeDiff, TimeUnit.MILLISECONDS) + 1L 22 | return stringResource(id = R.string.time_remaining, daysRemaining, stringResource(id = R.string.days)) 23 | } 24 | 25 | // If the medication is scheduled for today, calculate time remaining in hours and minutes 26 | val hoursRemaining = TimeUnit.HOURS.convert(timeDiff, TimeUnit.MILLISECONDS) 27 | val minutesRemaining = TimeUnit.MINUTES.convert(timeDiff, TimeUnit.MILLISECONDS) 28 | return when { 29 | hoursRemaining > 1 -> stringResource(id = R.string.time_remaining, hoursRemaining, stringResource(id = R.string.hours)) 30 | minutesRemaining > 1 -> stringResource(id = R.string.time_remaining, minutesRemaining, stringResource(id = R.string.days)) 31 | else -> stringResource(id = R.string.take_dose_now) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/waseefakhtar/doseapp/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.waseefakhtar.doseapp.util 2 | 3 | import androidx.navigation.NavHostController 4 | 5 | fun NavHostController.navigateSingleTop(route: String) { 6 | this.navigate(route) { 7 | popUpTo(route) 8 | launchSingleTop = true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/doctor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/drawable-xxxhdpi/doctor.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_capsule.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dose.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_drops.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 26 | 31 | 36 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_gel.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_spray.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 26 | 31 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_syrup.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tablet.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US -------------------------------------------------------------------------------- /app/src/main/res/values-b+it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Dose 3 | Home 4 | Calendario 5 | Aggiungi Orario 6 | Aggiungi Medicinale 7 | Nome Medicinale 8 | Dosaggio 9 | Ricorrenza 10 | Data di Fine 11 | Orari del Giorno 12 | Orari per il Medicinale 13 | Avanti 14 | Inserisci un valore valido per %s 15 | Fatto! 16 | 17 | I tuoi promemoria per %1$s sono pronti, con %2$s avviso %3$s fino a %4$s. 18 | I tuoi promemoria per %1$s sono pronti, con %2$s avvisi %3$s fino a %4$s. 19 | 20 | Conferma 21 | Prossima dose tra %1$s %2$s 22 | giorni 23 | ore 24 | minuti 25 | Prendi la dose adesso 26 | Preso 27 | Saltato 28 | Fatto 29 | Cronologia 30 | Ancora nessuna cronologia 31 | Indietro 32 | Aggiungi 33 | es. Risperdal, 4mg 34 | Errore 35 | es. 1 36 | Non puoi avere più di 99 dosaggi al giorno. 37 | Selezionato 38 | Stai selezionando %s volte che è superiore al numero di dosaggi. 39 | Il tuo piano per oggi 40 | %1$d su %2$d completati. 41 | Pausa dai Medicinali 42 | Nessun medicinale programmato per questa data. Fai una pausa e rilassati. 43 | Oggi 44 | Questa Settimana 45 | Settimana Prossima 46 | Notifiche 47 | Permesso di Notifica Richiesto 48 | Per non perdere mai la tua medicina, concedi il permesso di notifica. 49 | Consenti 50 | Preso alle %s 51 | Saltato alle %s 52 | Programmato alle %s 53 | %s è ora impostato per promemoria tempestivi. 54 | Medicina Registrata 55 | %1$d dose alle %2$s 56 | Promemoria Medicinale 57 | Notifiche per promemoria medicinale 58 | È ora di prendere la tua medicina, %s. Apri l\'app per registrarla. 59 | Annulla 60 | Ok 61 | Quotidiana 62 | Settimanalmente 63 | Mensile 64 | -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dosis 4 | Inicio 5 | Calendario 6 | Agregar medicación 7 | Nombre del medicamento 8 | Dosis 9 | Recurrencia 10 | Fecha final 11 | Horas del Día 12 | Hora(s) para la medicación 13 | Próximo 14 | Por favor ingresa un valor válido para %s 15 | ¡Todo listo! 16 | 17 | Tus %1$s recordatorios están listos, con %2$s alerta de %3$s hasta %4$s. 18 | Tus %1$s recordatorios están listos, con %2$s alertas de %3$s hasta %4$s. 19 | 20 | Confirmar 21 | Próxima dosis en %1$s %2$s 22 | días 23 | horas 24 | minutos 25 | Tome su dosis ahora 26 | Tomado 27 | Omitido 28 | Hecho 29 | Historial 30 | Aún no hay historial 31 | Atrás 32 | Agregar 33 | ej. Aspirina, 20mg 34 | Error 35 | ej. 1 36 | No puede tomar más de 99 dosis por día. 37 | Seleccionado 38 | Estás seleccionando %s veces de días que son más que la cantidad de dosis. 39 | Tu plan para hoy 40 | %d de %d completado. 41 | Pausa para la medicación 42 | No hay medicamentos programados para esta fecha. Tómate un descanso y relájate. 43 | Hoy 44 | Esta semana 45 | La próxima semana 46 | Notificaciones 47 | Se requiere permiso de notificación 48 | Para asegurarse de que nunca olvide su medicamento, otorgue el permiso de notificación. 49 | Permitir 50 | Tomado en %s 51 | Saltado en %s 52 | Programado a las %s 53 | %s ahora está configurado para recibir recordatorios oportunos. 54 | Medicación registrada 55 | %d dosis en %s 56 | Recordatorio de medicación 57 | Notificaciones de recordatorio de medicación 58 | Es hora de tomar tu medicación, %s. Abra la aplicación para iniciar sesión. 59 | Cancelar 60 | DE ACUERDO 61 | -------------------------------------------------------------------------------- /app/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | دوز 3 | خانه 4 | تقویم 5 | اضافه کردن دارو 6 | نام دارو 7 | دوز 8 | تکرار 9 | تاریخ پایان 10 | زمان ها در روز 11 | ساعت مصرف دارو 12 | بعدی 13 | لطفا مقدار درستی برای %s وارد کنید. 14 | همه تمام. 15 | تایید 16 | دوز بعدی در %1$s و %2$s 17 | روزها 18 | ساعت ها 19 | دقیقه ها 20 | الان دوز دارو را مصرف کنید 21 | مصرف شده 22 | فراموش شده 23 | تمام 24 | تاریخچه 25 | تاریخچه ای موجود نیست. 26 | قبلی 27 | اضافه کردن 28 | مثلا پروفن 4 میلی گرم 29 | ارور 30 | مثلا 3 31 | شما نمی توانید بیشتر از 99 دوز در روز داشته باشید. 32 | انتخاب شده 33 | شما %s تعداد در روز را انتخاب کرده اید که از تعداد کل دوزهای شما بیشتر است. 34 | برنامه شما برای امروز 35 | تعداد %d از %d تعداد تمام شده است. 36 | وقفه داروئی 37 | در این تاریخ دارویی برنامه ریزی نشده است. استراحت کنید. 38 | امروز 39 | این هفته 40 | هفته بعدی 41 | اطلاعیه ها 42 | دسترسی اطلاعیه ها مورد نیاز است. 43 | برای اینکه دارو خود را فراموش نکنید، اجازه این دسترسی را بدهید. 44 | هشدارها 45 | دسترسی هشدارها مورد نیاز است 46 | برای اطمینان از فراموش نکردن داروها، لطفا دسترسی هشدارها را بدهید. 47 | اجازه دادن 48 | مصرف شده در %s 49 | فراموش شده در %s 50 | برنامه ریزی شده در %s 51 | داروی %s برای هشدار دوره ای تنظیم شد. 52 | دارو ثبت شد 53 | تعداد %d دوز در تاریخ %s 54 | یادآور دارو 55 | اطلاعیه ها برای یادآور دارو 56 | الان زمان مصرف داروی %s شما است. برنامه را باز کنید تا مصرف دارو ثبت شود. 57 | بیخیال 58 | باشه 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #DEE1F9 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Dose 3 | Home 4 | Calendar 5 | Add Time 6 | Add Medication 7 | Medication Name 8 | Dosage 9 | Dose (Optional) 10 | Recurrence 11 | Frequency 12 | End Date 13 | Times of Day 14 | Time(s) for Medication 15 | Schedule 16 | Next 17 | Please enter a valid value for %s 18 | All done! 19 | 20 | Your %1$s reminders are ready, with %2$s alert %3$s until %4$s. 21 | Your %1$s reminders are ready, with %2$s alerts %3$s until %4$s. 22 | Your %1$s reminders are ready, with %2$s alerts %3$s until %4$s. 23 | 24 | Confirm 25 | Next dose in %1$s %2$s 26 | days 27 | hours 28 | minutes 29 | Take your dose now 30 | Taken 31 | Skipped 32 | Done 33 | History 34 | No history yet 35 | Back 36 | Add 37 | e.g. Risperdal, 4mg 38 | Error 39 | e.g. 1 40 | You cannot have more than 99 dosage per day. 41 | Selected 42 | You\'re selecting %s time(s) of days which is more than the number of dosage. 43 | Your plan for today 44 | %d of %d completed. 45 | Medication Break 46 | No medications scheduled for this date. Take a break and relax. 47 | Today 48 | This Week 49 | Next Week 50 | Notifications 51 | Notification Permission Required 52 | To ensure you never miss your medication, please grant the notification permission. 53 | Notifications 54 | Alarms Permission Required 55 | To ensure you never miss your medication, please grant the alarms permission. 56 | Allow 57 | Taken at %s 58 | Skipped at %s 59 | Scheduled at %s 60 | %s is now set up for timely reminders. 61 | Medication Logged 62 | %d dose at %s 63 | Medication Reminder 64 | Notifications for medication reminder 65 | It is time to take your medication, %s. Open the app to log it. 66 | Cancel 67 | OK 68 | Daily 69 | Weekly 70 | Monthly 71 | Duration 72 | Select Duration 73 | 74 | %d day 75 | %d days 76 | 77 | 78 | %d week 79 | %d weeks 80 | 81 | 82 | %d month 83 | %d months 84 | 85 | 86 | %d year 87 | %d years 88 | 89 | Everyday 90 | Every %s Days 91 | Every Week 92 | Every %s Weeks 93 | Every Month 94 | Type 95 | Tablet 96 | Capsule 97 | Syrup 98 | Drops 99 | Spray 100 | Gel 101 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |