├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 | 
2 |
3 |
4 |
Dose App 💊⏰
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/waseefakhtar/doseapp/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.waseefakhtar.doseapp
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
2 |
3 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
4 | plugins {
5 | alias(libs.plugins.android.application) apply false
6 | alias(libs.plugins.android.library) apply false
7 | alias(libs.plugins.jetbrains.kotlin.android) apply false
8 | alias(libs.plugins.ksp) apply false
9 | alias(libs.plugins.ktlint)
10 | alias(libs.plugins.hilt.android) apply false
11 | alias(libs.plugins.kotlin.parcelize) apply false
12 | alias(libs.plugins.google.services) apply false
13 | alias(libs.plugins.firebase.crashlytics) apply false
14 | alias(libs.plugins.kotlin.compose) apply false
15 | }
16 |
17 | subprojects {
18 | apply(plugin = "org.jlleitschuh.gradle.ktlint")
19 |
20 | configure {
21 | reporters {
22 | reporter(ReporterType.PLAIN)
23 | reporter(ReporterType.HTML)
24 | }
25 | }
26 | }
27 |
28 | task("clean", Delete::class) {
29 | delete(rootProject.buildDir)
30 | }
31 |
--------------------------------------------------------------------------------
/docs/demo/demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/demo/demo.mp4
--------------------------------------------------------------------------------
/docs/images/dose-splash-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/images/dose-splash-2.png
--------------------------------------------------------------------------------
/docs/images/dose-splash-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/images/dose-splash-3.jpg
--------------------------------------------------------------------------------
/docs/images/dose-splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/images/dose-splash.png
--------------------------------------------------------------------------------
/docs/images/play-store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/images/play-store.png
--------------------------------------------------------------------------------
/docs/release/app.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/release/app.apk
--------------------------------------------------------------------------------
/docs/screenshots/AddMedication.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/screenshots/AddMedication.png
--------------------------------------------------------------------------------
/docs/screenshots/Home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/screenshots/Home.png
--------------------------------------------------------------------------------
/docs/screenshots/MedicationConfirm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/docs/screenshots/MedicationConfirm.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | min-sdk-version = "21"
3 | compile-sdk-version = "35"
4 | target-sdk-version = "35"
5 | version-code = "10"
6 | version-name = "1.6.0"
7 | accompanistPermissions = "0.34.0"
8 | activity = "1.10.0"
9 | androidxHilt = "1.2.0"
10 | composeCompiler = "1.5.15"
11 | compose = "1.7.6"
12 | composeNavigation = "2.8.5"
13 | core = "1.15.0"
14 | espresso = "3.6.1"
15 | firebase = "33.8.0"
16 | firebaseCrashlyticsGradle = "3.0.2"
17 | gson = "2.10.1"
18 | gradle = "8.6.1"
19 | googleServices = "4.4.2"
20 | hilt = "2.49"
21 | junit = "4.13.2"
22 | junitExt = "1.2.1"
23 | kotlin = "2.0.21" # Must be aligned with KSP version
24 | ksp = "2.0.21-1.0.26" # KSP version must be aligned with kotlin https://github.com/google/ksp/releases
25 | ktlint = "10.3.0"
26 | lifecycle = "2.8.7"
27 | room = "2.6.1"
28 | material3 = "1.3.1"
29 | okhttp = "4.10.0"
30 |
31 | [libraries]
32 | # Accompanist
33 | accompanist-permission = { group="com.google.accompanist", name="accompanist-permissions", version.ref="accompanistPermissions" }
34 | # Android
35 | androidx-core-ktx = { group="androidx.core", name="core-ktx", version.ref="core" }
36 | # Compose
37 | compose-activity = { group="androidx.activity", name="activity-compose", version.ref="activity" }
38 | compose-fundation = { group="androidx.compose.foundation", name="foundation", version.ref="compose" }
39 | compose-material3 = { group="androidx.compose.material3", name="material3", version.ref = "material3" }
40 | compose-junit-ui = { group="androidx.compose.ui", name="ui-test-junit4", version.ref="compose" }
41 | compose-navigation = { group="androidx.navigation", name="navigation-compose", version.ref="composeNavigation" }
42 | compose-preview = { group="androidx.compose.ui", name="ui-tooling-preview", version.ref="compose" }
43 | compose-ui-tooling-debug = { group="androidx.compose.ui", name="ui-tooling", version.ref="compose" }
44 | compose-ui-test-manifest = { group="androidx.compose.ui", name="ui-test-manifest", version.ref="compose" }
45 | compose-ui = { group="androidx.compose.ui", name="ui", version.ref="compose" }
46 | # Firebase
47 | firebase-analytics = { group="com.google.firebase", name="firebase-analytics" }
48 | firebase-crashlytics = { group="com.google.firebase", name="firebase-crashlytics" }
49 | firebase-bom = { group="com.google.firebase", name="firebase-bom", version.ref="firebase" }
50 | # Gson
51 | gson = { group="com.google.code.gson", name="gson", version.ref="gson" }
52 | # Hilt
53 | hilt-android = { group="com.google.dagger", name="hilt-android", version.ref="hilt"}
54 | hilt-compiler = { group="com.google.dagger", name="hilt-compiler", version.ref="hilt"}
55 | hilt-androidx-compiler = { group= "androidx.hilt", name= "hilt-compiler", version.ref="androidxHilt"}
56 | hilt-navigation-compose = { group= "androidx.hilt", name= "hilt-navigation-compose", version.ref="androidxHilt"}
57 | # Lifecycle
58 | lifecycle-runtime-ktx = { group="androidx.lifecycle", name="lifecycle-runtime-ktx", version.ref="lifecycle" }
59 | lifecycle-viewmodel-compose = { group="androidx.lifecycle", name="lifecycle-viewmodel-compose", version.ref="lifecycle" }
60 | # OkHttp
61 | okhttp = { group="com.squareup.okhttp3", name="okhttp" }
62 | okhttp-logging-interceptor = { group="com.squareup.okhttp3", name="logging-interceptor" }
63 | okhttp-bom = { group="com.squareup.okhttp3", name="okhttp-bom", version.ref="okhttp" }
64 | # Room
65 | room-runtime = { group="androidx.room", name="room-runtime", version.ref="room"}
66 | room-ktx = { group="androidx.room", name="room-ktx", version.ref="room"}
67 | room-compiler = { group="androidx.room", name="room-compiler", version.ref="room"}
68 | # Tests
69 | junit = { group="junit", name="junit", version.ref="junit" }
70 | junit-ext = { group="androidx.test.ext", name="junit", version.ref="junitExt" }
71 | espresso-core = { group="androidx.test.espresso", name="espresso-core", version.ref="espresso" }
72 |
73 | [plugins]
74 | android-application = { id="com.android.application", version.ref="gradle" }
75 | android-library = { id="com.android.library", version.ref="gradle" }
76 | jetbrains-kotlin-android = { id="org.jetbrains.kotlin.android", version.ref="kotlin" }
77 | ktlint = { id="org.jlleitschuh.gradle.ktlint", version.ref="ktlint" }
78 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
79 | hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
80 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
81 | google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
82 | firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsGradle" }
83 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
84 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waseefakhtar/dose-android/cf6e2fc9b399b25b9a643a7544f4a7c1a660cf57/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 26 01:31:38 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 |
15 | dependencyResolutionManagement {
16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
17 | repositories {
18 | google()
19 | mavenCentral()
20 | }
21 | }
22 |
23 | rootProject.name = "Dose App"
24 | include(":app")
25 |
--------------------------------------------------------------------------------