├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── imnstudios │ │ └── runningapp │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── imnstudios │ │ │ └── runningapp │ │ │ ├── BaseApplication.kt │ │ │ ├── adapters │ │ │ └── RunAdapter.kt │ │ │ ├── db │ │ │ ├── Converters.kt │ │ │ ├── Run.kt │ │ │ ├── RunDao.kt │ │ │ └── RunningDatabase.kt │ │ │ ├── di │ │ │ ├── AppModule.kt │ │ │ └── ServiceModule.kt │ │ │ ├── other │ │ │ ├── Constants.kt │ │ │ ├── CustomMarkerView.kt │ │ │ ├── SortType.kt │ │ │ ├── TrackingUtility.kt │ │ │ └── ViewUtils.kt │ │ │ ├── repositories │ │ │ └── MainRepository.kt │ │ │ ├── services │ │ │ └── TrackingService.kt │ │ │ └── ui │ │ │ ├── MainActivity.kt │ │ │ ├── SplashScreenActivity.kt │ │ │ ├── fragments │ │ │ ├── CancelTrackingDialog.kt │ │ │ ├── LogInFragment.kt │ │ │ ├── RunFragment.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── SetupFragment.kt │ │ │ ├── StatisticsFragment.kt │ │ │ └── TrackingFragment.kt │ │ │ └── viewmodels │ │ │ ├── MainViewModel.kt │ │ │ ├── NetworkViewModel.kt │ │ │ └── StatisticsViewModel.kt │ └── res │ │ ├── drawable-v23 │ │ └── splash_screen_background.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bottom_nav_selector.xml │ │ ├── ic_add_black.xml │ │ ├── ic_close_white.xml │ │ ├── ic_delete.xml │ │ ├── ic_directions_run_black_24dp.xml │ │ ├── ic_graph.xml │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_pause_black_24dp.xml │ │ ├── ic_run.xml │ │ ├── ic_settings.xml │ │ ├── ic_stop_black_24dp.xml │ │ ├── splash_screen_background.xml │ │ └── view_bg.xml │ │ ├── font │ │ ├── poppins.ttf │ │ └── poppins_bold.ttf │ │ ├── layout-land │ │ ├── fragment_statistics.xml │ │ └── fragment_tracking.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── custom_spinner.xml │ │ ├── fragment_login.xml │ │ ├── fragment_run.xml │ │ ├── fragment_settings.xml │ │ ├── fragment_setup.xml │ │ ├── fragment_statistics.xml │ │ ├── fragment_tracking.xml │ │ ├── item_run.xml │ │ └── marker_view.xml │ │ ├── menu │ │ ├── bottom_nav_menu.xml │ │ └── toolbar_tracking_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ └── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── imnstudios │ └── runningapp │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /secrets.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Running App -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 20 | 22 | 23 | 24 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | xmlns:android 33 | 34 | ^$ 35 | 36 | 37 | 38 |
39 |
40 | 41 | 42 | 43 | xmlns:.* 44 | 45 | ^$ 46 | 47 | 48 | BY_NAME 49 | 50 |
51 |
52 | 53 | 54 | 55 | .*:id 56 | 57 | http://schemas.android.com/apk/res/android 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | .*:name 67 | 68 | http://schemas.android.com/apk/res/android 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | name 78 | 79 | ^$ 80 | 81 | 82 | 83 |
84 |
85 | 86 | 87 | 88 | style 89 | 90 | ^$ 91 | 92 | 93 | 94 |
95 |
96 | 97 | 98 | 99 | .* 100 | 101 | ^$ 102 | 103 | 104 | BY_NAME 105 | 106 |
107 |
108 | 109 | 110 | 111 | .* 112 | 113 | http://schemas.android.com/apk/res/android 114 | 115 | 116 | ANDROID_ATTRIBUTE_ORDER 117 | 118 |
119 |
120 | 121 | 122 | 123 | .* 124 | 125 | .* 126 | 127 | 128 | BY_NAME 129 | 130 |
131 |
132 |
133 |
134 | 135 | 137 |
138 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RUNNING APP 2 | as the name says, it is used for tracking a Run. 3 | This app is build based on Modern Android Libraries. 4 | 5 | 6 | 7 | # THIS APP IS BUILD USING 8 | * MVVM 9 | * DEPENDENCY INJECTION(DAGGER HILT) 10 | * COROUTINES 11 | * FIREBASE 12 | * KOTLIN 13 | * NAVIGATION COMPONENTS 14 | * GOOGLE MAPS API 15 | 16 | 17 | 18 | 19 | 20 | ## Download 21 | [Click here](https://firebasestorage.googleapis.com/v0/b/running-app-1747f.appspot.com/o/running-app.apk?alt=media&token=d73e7bd4-0299-4331-8117-afa1e6b91080) 22 | 23 | 24 | 25 | ## License 26 | ``` 27 | Copyright 2020 Nitheesh Ag 28 | 29 | Licensed under the Apache License, Version 2.0 (the "License"); 30 | you may not use this file except in compliance with the License. 31 | You may obtain a copy of the License at 32 | 33 | http://www.apache.org/licenses/LICENSE-2.0 34 | 35 | Unless required by applicable law or agreed to in writing, software 36 | distributed under the License is distributed on an "AS IS" BASIS, 37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 38 | See the License for the specific language governing permissions and 39 | limitations under the License. 40 | ``` 41 | 42 | ### Contact 43 | Contact me at +918907471155 or imnithish@live.com. 44 | 45 | [My Website](https://imnstudios.com/#/nitheeshag) 46 | 47 | Connect with me in [LinkedIn](https://www.linkedin.com/in/imnithish/) 48 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.json -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: "androidx.navigation.safeargs.kotlin" 6 | apply plugin: 'dagger.hilt.android.plugin' 7 | apply plugin: 'com.google.gms.google-services' 8 | 9 | android { 10 | compileSdkVersion 29 11 | 12 | defaultConfig { 13 | applicationId "com.imnstudios.runningapp" 14 | minSdkVersion 26 15 | targetSdkVersion 29 16 | versionCode 1 17 | versionName "1.0" 18 | manifestPlaceholders = [GOOGLE_MAPS_API_KEY:getApiKey()] 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | kotlinOptions { 36 | jvmTarget = JavaVersion.VERSION_1_8.toString() 37 | } 38 | } 39 | 40 | static def getApiKey(){ 41 | Properties props = new Properties() 42 | props.load(new FileInputStream(new File('secrets.properties'))) 43 | return props['GOOGLE_MAPS_API_KEY'] 44 | } 45 | 46 | dependencies { 47 | implementation fileTree(dir: "libs", include: ["*.jar"]) 48 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 49 | implementation 'androidx.core:core-ktx:1.3.1' 50 | implementation 'androidx.appcompat:appcompat:1.2.0' 51 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 52 | testImplementation 'junit:junit:4.12' 53 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 55 | 56 | // Material Design 57 | implementation 'com.google.android.material:material:1.3.0-alpha02' 58 | 59 | // Architectural Components 60 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" 61 | 62 | // Room 63 | implementation "androidx.room:room-runtime:2.2.5" 64 | kapt "androidx.room:room-compiler:2.2.5" 65 | 66 | // Kotlin Extensions and Coroutines support for Room 67 | implementation "androidx.room:room-ktx:2.2.5" 68 | 69 | // Coroutines 70 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5' 71 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5' 72 | 73 | // Coroutine Lifecycle Scopes 74 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" 75 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" 76 | 77 | // Navigation Components 78 | implementation "androidx.navigation:navigation-fragment-ktx:2.3.0" 79 | implementation "androidx.navigation:navigation-ui-ktx:2.3.0" 80 | 81 | // Glide 82 | implementation 'com.github.bumptech.glide:glide:4.11.0' 83 | kapt 'com.github.bumptech.glide:compiler:4.11.0' 84 | 85 | // Google Maps Location Services 86 | implementation 'com.google.android.gms:play-services-location:17.0.0' 87 | implementation 'com.google.android.gms:play-services-maps:17.0.0' 88 | 89 | // Dagger Core 90 | implementation "com.google.dagger:dagger:2.28" 91 | kapt "com.google.dagger:dagger-compiler:2.28" 92 | 93 | // Dagger Android 94 | api 'com.google.dagger:dagger-android:2.23.2' 95 | api 'com.google.dagger:dagger-android-support:2.23.2' 96 | kapt 'com.google.dagger:dagger-android-processor:2.23.2' 97 | 98 | // Activity KTX for viewModels() 99 | implementation "androidx.activity:activity-ktx:1.1.0" 100 | 101 | //Dagger - Hilt 102 | implementation "com.google.dagger:hilt-android:2.28-alpha" 103 | kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" 104 | 105 | implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02" 106 | kapt "androidx.hilt:hilt-compiler:1.0.0-alpha02" 107 | 108 | // Easy Permissions 109 | implementation 'pub.devrel:easypermissions:3.0.0' 110 | 111 | // Timber 112 | implementation 'com.jakewharton.timber:timber:4.7.1' 113 | 114 | // MPAndroidChart 115 | implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' 116 | 117 | implementation 'android.arch.lifecycle:extensions:1.1.1' 118 | 119 | 120 | implementation 'com.google.firebase:firebase-analytics-ktx:17.5.0' 121 | implementation 'com.firebaseui:firebase-ui-auth:6.2.0' 122 | implementation 'com.google.firebase:firebase-firestore:21.5.0' 123 | 124 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1' 125 | } -------------------------------------------------------------------------------- /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/src/androidTest/java/com/imnstudios/runningapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.imnstudios.runningapp", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 43 | 44 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/7d1a0da914babeba7a1f794dfcb3404dd51237d2/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | import timber.log.Timber 6 | 7 | @HiltAndroidApp 8 | class BaseApplication: Application() { 9 | 10 | 11 | override fun onCreate() { 12 | super.onCreate() 13 | Timber.plant(Timber.DebugTree()) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/adapters/RunAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.adapters 2 | 3 | import android.graphics.BitmapFactory 4 | import android.util.Log 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.Toast 9 | import androidx.recyclerview.widget.AsyncListDiffer 10 | import androidx.recyclerview.widget.DiffUtil 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.bumptech.glide.Glide 13 | import com.imnstudios.runningapp.R 14 | import com.imnstudios.runningapp.db.Run 15 | import com.imnstudios.runningapp.other.TrackingUtility 16 | import kotlinx.android.synthetic.main.item_run.view.* 17 | import timber.log.Timber 18 | import java.lang.Exception 19 | import java.text.SimpleDateFormat 20 | import java.util.* 21 | 22 | 23 | class RunAdapter : RecyclerView.Adapter() { 24 | 25 | inner class RunViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) 26 | 27 | val diffCallback = object : DiffUtil.ItemCallback() { 28 | override fun areItemsTheSame(oldItem: Run, newItem: Run): Boolean { 29 | return oldItem.id == newItem.id 30 | } 31 | 32 | override fun areContentsTheSame(oldItem: Run, newItem: Run): Boolean { 33 | return oldItem.hashCode() == newItem.hashCode() 34 | //hashcode produces a value 35 | } 36 | 37 | } 38 | 39 | val differ = AsyncListDiffer(this, diffCallback) 40 | 41 | fun submitList(list: List) = differ.submitList(list) 42 | 43 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RunViewHolder { 44 | return RunViewHolder( 45 | LayoutInflater.from(parent.context).inflate( 46 | R.layout.item_run, 47 | parent, 48 | false 49 | ) 50 | ) 51 | } 52 | 53 | override fun getItemCount(): Int { 54 | return differ.currentList.size 55 | } 56 | 57 | override fun onBindViewHolder(holder: RunViewHolder, position: Int) { 58 | val run = differ.currentList[position] 59 | holder.itemView.apply { 60 | 61 | try { 62 | val encodeByte: ByteArray = Base64.getDecoder().decode(run.img) 63 | val bitmap = BitmapFactory.decodeByteArray(encodeByte, 0, encodeByte.size) 64 | Glide.with(this).load(bitmap).into(ivRunImage) 65 | } catch (e: Exception) { 66 | Timber.d(e.toString()) 67 | } 68 | 69 | val calender = Calendar.getInstance().apply { 70 | timeInMillis = run.timestamp 71 | } 72 | val dateFormat = SimpleDateFormat("dd.MM.YY", Locale.getDefault()) 73 | tvDate.text = dateFormat.format(calender.time) 74 | 75 | val avgSpeed = "${run.avgSpeedInKMH}km/h" 76 | tvAvgSpeed.text = avgSpeed 77 | 78 | val distanceInKm = "${run.distanceInMeters / 1000f}km" 79 | tvDistance.text = distanceInKm 80 | 81 | tvTime.text = TrackingUtility.getFormattedStopWatchTime(run.timeInMillis) 82 | 83 | val caloriesBurned = "${run.caloriesBurned}kcal" 84 | tvCalories.text = caloriesBurned 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/db/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.db 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import androidx.room.TypeConverter 6 | import java.io.ByteArrayOutputStream 7 | 8 | class Converters { 9 | 10 | @TypeConverter 11 | fun toBitmap(bytes: ByteArray): Bitmap { 12 | return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) 13 | } 14 | 15 | @TypeConverter 16 | fun fromBitmap(bmp: Bitmap): ByteArray { 17 | val outputStream = ByteArrayOutputStream() 18 | bmp.compress(Bitmap.CompressFormat.PNG, 100, outputStream) 19 | return outputStream.toByteArray() 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/db/Run.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.db 2 | 3 | import android.graphics.Bitmap 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "running_table") 8 | data class Run( 9 | @PrimaryKey 10 | var id: String = "", 11 | var img: String? = "", 12 | var timestamp: Long = 0L, 13 | var avgSpeedInKMH: Float = 0f, 14 | var distanceInMeters: Int = 0, 15 | var timeInMillis: Long = 0L, 16 | var caloriesBurned: Int = 0 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/db/RunDao.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.db 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.* 5 | 6 | 7 | @Dao 8 | interface RunDao { 9 | @Insert(onConflict = OnConflictStrategy.REPLACE) 10 | suspend fun insertRun(run: Run) 11 | 12 | @Delete 13 | suspend fun deleteRun(run: Run) 14 | 15 | @Query("SELECT * FROM running_table ORDER BY timestamp DESC") 16 | fun getAllRunsSortedByDate(): LiveData> 17 | 18 | @Query("SELECT * FROM running_table ORDER BY timeInMillis DESC") 19 | fun getAllRunsSortedByTimeInMillis(): LiveData> 20 | 21 | @Query("SELECT * FROM running_table ORDER BY caloriesBurned DESC") 22 | fun getAllRunsSortedByCaloriesBurned(): LiveData> 23 | 24 | @Query("SELECT * FROM running_table ORDER BY avgSpeedInKMH DESC") 25 | fun getAllRunsSortedByAvgSpeed(): LiveData> 26 | 27 | @Query("SELECT * FROM running_table ORDER BY distanceInMeters DESC") 28 | fun getAllRunsSortedByDistance(): LiveData> 29 | 30 | @Query("SELECT SUM(timeInMillis) FROM running_table") 31 | fun getTotalTimeInMillis(): LiveData 32 | 33 | @Query("SELECT SUM(caloriesBurned) FROM running_table") 34 | fun getTotalCaloriesBurned(): LiveData 35 | 36 | @Query("SELECT SUM(distanceInMeters) FROM running_table") 37 | fun getTotalDistance(): LiveData 38 | 39 | @Query("SELECT AVG(avgSpeedInKMH) FROM running_table") 40 | fun getTotalAvgSpeed(): LiveData 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/db/RunningDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | 7 | @Database( 8 | entities = [Run::class], 9 | version = 2 10 | ) 11 | //@TypeConverters(Converters::class) 12 | abstract class RunningDatabase : RoomDatabase() { 13 | 14 | abstract fun getRunDao(): RunDao 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.di 2 | 3 | import android.content.Context 4 | import android.content.Context.MODE_PRIVATE 5 | import android.content.SharedPreferences 6 | import androidx.room.Room 7 | import com.imnstudios.runningapp.db.RunningDatabase 8 | import com.imnstudios.runningapp.other.Constants.KEY_FIRST_TIME_TOGGLE 9 | import com.imnstudios.runningapp.other.Constants.KEY_NAME 10 | import com.imnstudios.runningapp.other.Constants.KEY_WEIGHT 11 | import com.imnstudios.runningapp.other.Constants.RUNNING_DATABASE_NAME 12 | import com.imnstudios.runningapp.other.Constants.SHARED_PREFERENCES_NAME 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.hilt.InstallIn 16 | import dagger.hilt.android.components.ApplicationComponent 17 | import dagger.hilt.android.qualifiers.ApplicationContext 18 | import javax.inject.Singleton 19 | 20 | @Module 21 | @InstallIn(ApplicationComponent::class) 22 | object AppModule { 23 | 24 | @Singleton 25 | @Provides 26 | fun provideRunningDatabase( 27 | @ApplicationContext app: Context 28 | ) = Room.databaseBuilder( 29 | app, 30 | RunningDatabase::class.java, 31 | RUNNING_DATABASE_NAME 32 | ).fallbackToDestructiveMigration().build() 33 | 34 | @Singleton 35 | @Provides 36 | fun provideRunDao(db: RunningDatabase) = db.getRunDao() 37 | 38 | 39 | @Singleton 40 | @Provides 41 | fun provideSharedPreferences( 42 | @ApplicationContext app: Context 43 | ) = app.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) 44 | 45 | @Singleton 46 | @Provides 47 | fun provideName(sharedPref: SharedPreferences) = sharedPref.getString(KEY_NAME, "") ?: "" 48 | 49 | @Singleton 50 | @Provides 51 | fun provideWeight(sharedPref: SharedPreferences) = sharedPref.getFloat(KEY_WEIGHT, 80f) 52 | 53 | @Singleton 54 | @Provides 55 | fun provideFirstTimeToggle(sharedPref: SharedPreferences) = 56 | sharedPref.getBoolean(KEY_FIRST_TIME_TOGGLE, true) 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/di/ServiceModule.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.di 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.core.app.NotificationCompat 7 | import com.google.android.gms.location.FusedLocationProviderClient 8 | import com.imnstudios.runningapp.R 9 | import com.imnstudios.runningapp.other.Constants 10 | import com.imnstudios.runningapp.ui.MainActivity 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.android.components.ServiceComponent 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.android.scopes.ServiceScoped 17 | 18 | @Module 19 | @InstallIn(ServiceComponent::class) 20 | object ServiceModule { 21 | 22 | @ServiceScoped 23 | @Provides 24 | fun provideFusedLocationProviderClient( 25 | @ApplicationContext app: Context 26 | ) = FusedLocationProviderClient(app) 27 | 28 | 29 | @ServiceScoped 30 | @Provides 31 | fun provideMainActivityPendingIntent( 32 | @ApplicationContext app: Context 33 | ) = PendingIntent.getActivity( 34 | app, 35 | 0, 36 | Intent(app, MainActivity::class.java).also { 37 | it.action = Constants.ACTION_SHOW_TRACKING_FRAGMENT 38 | }, 39 | PendingIntent.FLAG_UPDATE_CURRENT 40 | ) 41 | 42 | 43 | @ServiceScoped 44 | @Provides 45 | fun provideBaseNotificationBuilder( 46 | @ApplicationContext app: Context, 47 | pendingIntent: PendingIntent 48 | ) = NotificationCompat.Builder(app, Constants.NOTIFICATION_CHANNEL_ID) 49 | .setAutoCancel(false) 50 | .setOngoing(true) 51 | .setSmallIcon(R.drawable.ic_directions_run_black_24dp) 52 | .setContentTitle("Running App") 53 | .setContentText("00:00:00") 54 | .setContentIntent(pendingIntent) 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/other/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.other 2 | 3 | import android.graphics.Color 4 | 5 | object Constants { 6 | 7 | const val RUNNING_DATABASE_NAME = "running_db" 8 | const val REQUEST_CODE_LOCATION_PERMISSION = 0 9 | 10 | const val ACTION_START_OR_RESUME_SERVICE = "ACTION_START_OR_RESUME_SERVICE" 11 | const val ACTION_PAUSE_SERVICE = "ACTION_PAUSE_SERVICE" 12 | const val ACTION_STOP_SERVICE = "ACTION_STOP_SERVICE" 13 | const val ACTION_SHOW_TRACKING_FRAGMENT = "ACTION_SHOW_TRACKING_FRAGMENT" 14 | 15 | const val TIMER_UPDATE_INTERVAL = 50L 16 | const val LOCATION_UPDATE_INTERVAL = 5000L 17 | const val FASTEST_LOCATION_INTERVAL = 2000L 18 | 19 | const val POLYLINE_COLOR = Color.RED 20 | const val POLYLINE_WIDTH = 8f 21 | const val MAP_ZOOM = 15f 22 | 23 | const val NOTIFICATION_CHANNEL_ID = "tracking_channel" 24 | const val NOTIFICATION_CHANNEL_NAME = "Tracking" 25 | const val NOTIFICATION_ID = 1 26 | 27 | const val SHARED_PREFERENCES_NAME = "sharedPref" 28 | 29 | const val KEY_FIRST_TIME_TOGGLE = "KEY_FIRST_TIME_TOGGLE" 30 | const val KEY_NAME = "KEY_NAME" 31 | const val KEY_WEIGHT = "KEY_WEIGHT" 32 | 33 | 34 | const val RC_SIGN_IN = 1 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/other/CustomMarkerView.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.other 2 | 3 | import android.content.Context 4 | 5 | import com.github.mikephil.charting.components.MarkerView 6 | import com.github.mikephil.charting.data.Entry 7 | import com.github.mikephil.charting.highlight.Highlight 8 | import com.github.mikephil.charting.utils.MPPointF 9 | import com.imnstudios.runningapp.db.Run 10 | import kotlinx.android.synthetic.main.marker_view.view.* 11 | import java.text.SimpleDateFormat 12 | import java.util.* 13 | 14 | class CustomMarkerView( 15 | val runs: List, 16 | c: Context, 17 | layoutId: Int 18 | ) : MarkerView(c, layoutId) { 19 | 20 | override fun getOffset(): MPPointF { 21 | return MPPointF(-width / 2f, -height.toFloat()) 22 | } 23 | 24 | override fun refreshContent(e: Entry?, highlight: Highlight?) { 25 | super.refreshContent(e, highlight) 26 | if (e == null) { 27 | return 28 | } 29 | val curRunId = e.x.toInt() 30 | val run = runs[curRunId] 31 | 32 | val calendar = Calendar.getInstance().apply { 33 | timeInMillis = run.timestamp 34 | } 35 | val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.getDefault()) 36 | tvDate.text = dateFormat.format(calendar.time) 37 | 38 | val avgSpeed = "${run.avgSpeedInKMH}km/h" 39 | tvAvgSpeed.text = avgSpeed 40 | 41 | val distanceInKm = "${run.distanceInMeters / 1000f}km" 42 | tvDistance.text = distanceInKm 43 | 44 | tvDuration.text = TrackingUtility.getFormattedStopWatchTime(run.timeInMillis) 45 | 46 | val caloriesBurned = "${run.caloriesBurned}kcal" 47 | tvCaloriesBurned.text = caloriesBurned 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/other/SortType.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.other 2 | 3 | enum class SortType { 4 | DATE, RUNNING_TIME, AVG_SPEED, DISTANCE, CALORIES_BURNED 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/other/TrackingUtility.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.other 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.location.Location 6 | import android.os.Build 7 | import com.imnstudios.runningapp.services.Polyline 8 | import pub.devrel.easypermissions.EasyPermissions 9 | import java.util.concurrent.TimeUnit 10 | 11 | object TrackingUtility { 12 | 13 | fun hasLocationPermissions(context: Context) = 14 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 15 | EasyPermissions.hasPermissions( 16 | context, 17 | Manifest.permission.ACCESS_FINE_LOCATION, 18 | Manifest.permission.ACCESS_COARSE_LOCATION 19 | ) 20 | } else { 21 | EasyPermissions.hasPermissions( 22 | context, 23 | Manifest.permission.ACCESS_FINE_LOCATION, 24 | Manifest.permission.ACCESS_COARSE_LOCATION, 25 | Manifest.permission.ACCESS_BACKGROUND_LOCATION 26 | ) 27 | } 28 | 29 | fun calculatePolylineLength(polyline: Polyline): Float { 30 | var distance = 0f 31 | for (i in 0..polyline.size - 2) { 32 | val pos1 = polyline[i] 33 | val pos2 = polyline[i + 1] 34 | 35 | val result = FloatArray(1) 36 | Location.distanceBetween( 37 | pos1.latitude, 38 | pos1.longitude, 39 | pos2.latitude, 40 | pos2.longitude, 41 | result 42 | ) 43 | distance += result[0] 44 | } 45 | return distance 46 | } 47 | 48 | fun getFormattedStopWatchTime(ms: Long, includeMillis: Boolean = false): String { 49 | var milliseconds = ms 50 | val hours = TimeUnit.MILLISECONDS.toHours(milliseconds) 51 | milliseconds -= TimeUnit.HOURS.toMillis(hours) 52 | val minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) 53 | milliseconds -= TimeUnit.MINUTES.toMillis(minutes) 54 | val seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) 55 | if (!includeMillis) { 56 | return "${if (hours < 10) "0" else ""}$hours:" + 57 | "${if (minutes < 10) "0" else ""}$minutes:" + 58 | "${if (seconds < 10) "0" else ""}$seconds" 59 | } 60 | milliseconds -= TimeUnit.SECONDS.toMillis(seconds) 61 | milliseconds /= 10 62 | return "${if (hours < 10) "0" else ""}$hours:" + 63 | "${if (minutes < 10) "0" else ""}$minutes:" + 64 | "${if (seconds < 10) "0" else ""}$seconds:" + 65 | "${if (milliseconds < 10) "0" else ""}$milliseconds" 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/other/ViewUtils.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.other 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.widget.Toast 6 | import com.google.android.material.snackbar.Snackbar 7 | 8 | fun Context.toast(message: String) { 9 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); 10 | } 11 | 12 | fun View.show() { 13 | visibility = View.VISIBLE 14 | } 15 | 16 | fun View.hide() { 17 | visibility = View.GONE 18 | } 19 | 20 | fun View.snackbar(message: String) { 21 | Snackbar.make(this, message, Snackbar.LENGTH_SHORT).show() 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/repositories/MainRepository.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.repositories 2 | 3 | import com.imnstudios.runningapp.db.Run 4 | import com.imnstudios.runningapp.db.RunDao 5 | import javax.inject.Inject 6 | 7 | class MainRepository @Inject constructor( 8 | private val runDao: RunDao 9 | ) { 10 | suspend fun insertRun(run: Run) = runDao.insertRun(run) 11 | 12 | suspend fun deleteRun(run: Run) = runDao.deleteRun(run) 13 | 14 | fun getAllRunsSortedByDate() = runDao.getAllRunsSortedByDate() 15 | 16 | fun getAllRunsSortedByDistance() = runDao.getAllRunsSortedByDistance() 17 | 18 | fun getAllRunsSortedByTimeInMillis() = runDao.getAllRunsSortedByTimeInMillis() 19 | 20 | fun getAllRunsSortedByAvgSpeed() = runDao.getAllRunsSortedByAvgSpeed() 21 | 22 | fun getAllRunsSortedByCaloriesBurned() = runDao.getAllRunsSortedByCaloriesBurned() 23 | 24 | fun getTotalAvgSpeed() = runDao.getTotalAvgSpeed() 25 | 26 | fun getTotalDistance() = runDao.getTotalDistance() 27 | 28 | fun getTotalCaloriesBurned() = runDao.getTotalCaloriesBurned() 29 | 30 | fun getTotalTimeInMillis() = runDao.getTotalTimeInMillis() 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/services/TrackingService.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.services 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.NotificationManager.IMPORTANCE_LOW 7 | import android.app.PendingIntent 8 | import android.app.PendingIntent.FLAG_UPDATE_CURRENT 9 | import android.content.Context 10 | import android.content.Intent 11 | import android.location.Location 12 | import android.os.Build 13 | import android.os.Looper 14 | import androidx.annotation.RequiresApi 15 | import androidx.core.app.NotificationCompat 16 | import androidx.lifecycle.LifecycleService 17 | import androidx.lifecycle.MutableLiveData 18 | import androidx.lifecycle.Observer 19 | import com.google.android.gms.location.FusedLocationProviderClient 20 | import com.google.android.gms.location.LocationCallback 21 | import com.google.android.gms.location.LocationRequest 22 | import com.google.android.gms.location.LocationRequest.PRIORITY_HIGH_ACCURACY 23 | import com.google.android.gms.location.LocationResult 24 | import com.google.android.gms.maps.model.LatLng 25 | import com.imnstudios.runningapp.R 26 | import com.imnstudios.runningapp.other.Constants.ACTION_PAUSE_SERVICE 27 | import com.imnstudios.runningapp.other.Constants.ACTION_SHOW_TRACKING_FRAGMENT 28 | import com.imnstudios.runningapp.other.Constants.ACTION_START_OR_RESUME_SERVICE 29 | import com.imnstudios.runningapp.other.Constants.ACTION_STOP_SERVICE 30 | import com.imnstudios.runningapp.other.Constants.FASTEST_LOCATION_INTERVAL 31 | import com.imnstudios.runningapp.other.Constants.LOCATION_UPDATE_INTERVAL 32 | import com.imnstudios.runningapp.other.Constants.NOTIFICATION_CHANNEL_ID 33 | import com.imnstudios.runningapp.other.Constants.NOTIFICATION_CHANNEL_NAME 34 | import com.imnstudios.runningapp.other.Constants.NOTIFICATION_ID 35 | import com.imnstudios.runningapp.other.Constants.TIMER_UPDATE_INTERVAL 36 | import com.imnstudios.runningapp.other.TrackingUtility 37 | import com.imnstudios.runningapp.ui.MainActivity 38 | import dagger.hilt.android.AndroidEntryPoint 39 | import kotlinx.coroutines.CoroutineScope 40 | import kotlinx.coroutines.Dispatchers 41 | import kotlinx.coroutines.delay 42 | import kotlinx.coroutines.launch 43 | import timber.log.Timber 44 | import javax.inject.Inject 45 | 46 | 47 | typealias Polyline = MutableList 48 | typealias Polylines = MutableList 49 | 50 | @AndroidEntryPoint 51 | class TrackingService : LifecycleService() { 52 | 53 | var isFirstRun = true 54 | var serviceKilled = false 55 | 56 | @Inject 57 | lateinit var fusedLocationProviderClient: FusedLocationProviderClient 58 | 59 | private val timeRunInSeconds = MutableLiveData() 60 | 61 | @Inject 62 | lateinit var baseNotificationBuilder: NotificationCompat.Builder 63 | 64 | lateinit var curNotificationBuilder: NotificationCompat.Builder 65 | 66 | companion object { 67 | val isTracking = MutableLiveData() 68 | val pathPoints = MutableLiveData() 69 | val timeRunInMillis = MutableLiveData() 70 | } 71 | 72 | private fun postInitialValues() { 73 | isTracking.postValue(false) 74 | pathPoints.postValue(mutableListOf()) 75 | timeRunInSeconds.postValue(0L) 76 | timeRunInMillis.postValue(0L) 77 | } 78 | 79 | override fun onCreate() { 80 | super.onCreate() 81 | curNotificationBuilder = baseNotificationBuilder 82 | postInitialValues() 83 | fusedLocationProviderClient = FusedLocationProviderClient(this) 84 | 85 | isTracking.observe(this, Observer { 86 | updateLocationTracking(it) 87 | updateNotificationTrackingState(it) 88 | }) 89 | } 90 | 91 | private fun killService() { 92 | serviceKilled = true 93 | isFirstRun = true 94 | pauseService() 95 | postInitialValues() 96 | stopForeground(true) 97 | stopSelf() 98 | } 99 | 100 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 101 | intent.let { 102 | when (it.action) { 103 | ACTION_START_OR_RESUME_SERVICE -> { 104 | if (isFirstRun) { 105 | startForegroundService() 106 | isFirstRun = false 107 | } else { 108 | Timber.d("Resuming service...") 109 | startTimer() 110 | } 111 | } 112 | ACTION_PAUSE_SERVICE -> { 113 | Timber.d("Paused service") 114 | pauseService() 115 | } 116 | ACTION_STOP_SERVICE -> { 117 | Timber.d("Stopped service") 118 | killService() 119 | } 120 | } 121 | } 122 | 123 | return super.onStartCommand(intent, flags, startId) 124 | } 125 | 126 | 127 | private var isTimerEnabled = false 128 | private var lapTime = 0L 129 | private var timeRun = 0L 130 | private var timeStarted = 0L 131 | private var lastSecondTimestamp = 0L 132 | 133 | private fun startTimer() { 134 | addEmptyPolyline() 135 | isTracking.postValue(true) 136 | timeStarted = System.currentTimeMillis() 137 | isTimerEnabled = true 138 | CoroutineScope(Dispatchers.Main).launch { 139 | while (isTracking.value!!) { 140 | //time difference b/w now and started time 141 | lapTime = System.currentTimeMillis() - timeStarted 142 | 143 | //new lap time 144 | timeRunInMillis.postValue(timeRun + lapTime) 145 | if (timeRunInMillis.value!! >= lastSecondTimestamp + 1000L) { 146 | timeRunInSeconds.postValue(timeRunInSeconds.value!! + 1) 147 | lastSecondTimestamp += 1000L 148 | } 149 | delay(TIMER_UPDATE_INTERVAL) 150 | } 151 | timeRun += lapTime 152 | } 153 | } 154 | 155 | private fun pauseService() { 156 | isTracking.postValue(false) 157 | isTimerEnabled = false 158 | } 159 | 160 | private fun updateNotificationTrackingState(isTracking: Boolean) { 161 | val notificationActionText = if (isTracking) "Pause" else "Resume" 162 | val pendingIntent = if (isTracking) { 163 | val pauseIntent = Intent(this, TrackingService::class.java).apply { 164 | action = ACTION_PAUSE_SERVICE 165 | } 166 | PendingIntent.getService(this, 1, pauseIntent, FLAG_UPDATE_CURRENT) 167 | } else { 168 | val resumeIntent = Intent(this, TrackingService::class.java).apply { 169 | action = ACTION_START_OR_RESUME_SERVICE 170 | } 171 | PendingIntent.getService(this, 2, resumeIntent, FLAG_UPDATE_CURRENT) 172 | } 173 | val notificationManager = 174 | getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 175 | 176 | 177 | //remove all the actions currently in notification 178 | curNotificationBuilder.javaClass.getDeclaredField("mActions").apply { 179 | isAccessible = true 180 | set(curNotificationBuilder, ArrayList()) 181 | } 182 | 183 | if (!serviceKilled) { 184 | curNotificationBuilder = baseNotificationBuilder 185 | .addAction(R.drawable.ic_pause_black_24dp, notificationActionText, pendingIntent) 186 | notificationManager.notify(NOTIFICATION_ID, curNotificationBuilder.build()) 187 | } 188 | 189 | 190 | } 191 | 192 | @SuppressLint("MissingPermission") 193 | private fun updateLocationTracking(isTracking: Boolean) { 194 | if (isTracking) { 195 | if (TrackingUtility.hasLocationPermissions(this)) { 196 | val request = LocationRequest().apply { 197 | interval = LOCATION_UPDATE_INTERVAL 198 | fastestInterval = FASTEST_LOCATION_INTERVAL 199 | priority = PRIORITY_HIGH_ACCURACY 200 | } 201 | fusedLocationProviderClient.requestLocationUpdates( 202 | request, 203 | locationCallback, 204 | Looper.getMainLooper() 205 | ) 206 | } 207 | } else { 208 | fusedLocationProviderClient.removeLocationUpdates(locationCallback) 209 | } 210 | } 211 | 212 | val locationCallback = object : LocationCallback() { 213 | override fun onLocationResult(result: LocationResult?) { 214 | super.onLocationResult(result) 215 | if (isTracking.value!!) { 216 | result?.locations?.let { locations -> 217 | for (location in locations) { 218 | addPathPoint(location) 219 | Timber.d("New Location: ${location.latitude}, ${location.longitude}") 220 | } 221 | } 222 | } 223 | } 224 | } 225 | 226 | private fun addPathPoint(location: Location?) { 227 | location?.let { 228 | val pos = LatLng(location.latitude, location.longitude) 229 | pathPoints.value?.apply { 230 | last().add(pos) 231 | pathPoints.postValue(this) 232 | } 233 | } 234 | } 235 | 236 | private fun addEmptyPolyline() = pathPoints.value?.apply { 237 | add(mutableListOf()) 238 | pathPoints.postValue(this) 239 | } ?: pathPoints.postValue(mutableListOf(mutableListOf())) 240 | 241 | 242 | private fun startForegroundService() { 243 | startTimer() 244 | isTracking.postValue(true) 245 | val notificationManager = 246 | getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 247 | 248 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 249 | createNotificationChannel(notificationManager) 250 | } 251 | 252 | //usual way 253 | // val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) 254 | // .setAutoCancel(false) 255 | // .setOngoing(true) 256 | // .setSmallIcon(R.drawable.ic_directions_run_black_24dp) 257 | // .setContentTitle("Running App") 258 | // .setContentText("00:00:00") 259 | // .setContentIntent(getMainActivityPendingIntent()) 260 | // startForeground(NOTIFICATION_ID, notificationBuilder.build()) 261 | 262 | //dependency injected 263 | 264 | startForeground(NOTIFICATION_ID, baseNotificationBuilder.build()) 265 | 266 | timeRunInSeconds.observe(this, Observer { 267 | if (!serviceKilled) { 268 | val notification = curNotificationBuilder 269 | .setContentText(TrackingUtility.getFormattedStopWatchTime(it * 1000L)) 270 | notificationManager.notify(NOTIFICATION_ID, notification.build()) 271 | } 272 | }) 273 | } 274 | 275 | //usual way. now it is contained in baseNotificationBuilder 276 | // private fun getMainActivityPendingIntent() = 277 | // PendingIntent.getActivity( 278 | // this, 279 | // 0, 280 | // Intent(this, MainActivity::class.java).also { 281 | // it.action = ACTION_SHOW_TRACKING_FRAGMENT 282 | // }, 283 | // FLAG_UPDATE_CURRENT 284 | // ) 285 | 286 | @RequiresApi(Build.VERSION_CODES.O) 287 | private fun createNotificationChannel(notificationManager: NotificationManager) { 288 | val channel = NotificationChannel( 289 | NOTIFICATION_CHANNEL_ID, 290 | NOTIFICATION_CHANNEL_NAME, 291 | IMPORTANCE_LOW 292 | ) 293 | 294 | notificationManager.createNotificationChannel(channel) 295 | } 296 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui 2 | 3 | import android.content.Intent 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.view.View 7 | import androidx.core.view.isVisible 8 | import androidx.navigation.fragment.findNavController 9 | import androidx.navigation.ui.setupWithNavController 10 | import com.google.firebase.auth.FirebaseAuth 11 | import com.google.firebase.firestore.FirebaseFirestore 12 | import com.google.firebase.firestore.FirebaseFirestoreSettings 13 | import com.imnstudios.runningapp.R 14 | import com.imnstudios.runningapp.db.RunDao 15 | import com.imnstudios.runningapp.other.Constants.ACTION_SHOW_TRACKING_FRAGMENT 16 | import dagger.hilt.android.AndroidEntryPoint 17 | import kotlinx.android.synthetic.main.activity_main.* 18 | import javax.inject.Inject 19 | 20 | @AndroidEntryPoint 21 | class MainActivity : AppCompatActivity() { 22 | 23 | companion object { 24 | lateinit var auth: FirebaseAuth 25 | lateinit var firestoreDb: FirebaseFirestore 26 | } 27 | 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | auth = FirebaseAuth.getInstance() 32 | firestoreDb = FirebaseFirestore.getInstance() 33 | val firebaseSettings: FirebaseFirestoreSettings = 34 | FirebaseFirestoreSettings.Builder().setPersistenceEnabled(true).build() 35 | firestoreDb.firestoreSettings = firebaseSettings 36 | setContentView(R.layout.activity_main) 37 | 38 | navigateToTrackingFragmentIfNeeded(intent) 39 | 40 | setSupportActionBar(toolbar) 41 | bottomNavigationView.setupWithNavController(navHostFragment.findNavController()) 42 | bottomNavigationView.setOnNavigationItemReselectedListener { /* NO-OP */ } 43 | 44 | 45 | navHostFragment.findNavController() 46 | .addOnDestinationChangedListener { _, destination, _ -> 47 | when (destination.id) { 48 | R.id.settingsFragment, R.id.runFragment, R.id.statisticsFragment -> { 49 | bottomNavigationView.visibility = View.VISIBLE 50 | appBarLayout.visibility = View.VISIBLE 51 | } 52 | R.id.trackingFragment -> { 53 | bottomNavigationView.visibility = View.GONE 54 | appBarLayout.visibility = View.VISIBLE 55 | } 56 | R.id.logInFragment, R.id.setupFragment -> { 57 | bottomNavigationView.visibility = View.GONE 58 | appBarLayout.visibility = View.GONE 59 | } 60 | 61 | } 62 | } 63 | } 64 | 65 | override fun onNewIntent(intent: Intent?) { 66 | super.onNewIntent(intent) 67 | navigateToTrackingFragmentIfNeeded(intent) 68 | } 69 | 70 | private fun navigateToTrackingFragmentIfNeeded(intent: Intent?) { 71 | if (intent?.action == ACTION_SHOW_TRACKING_FRAGMENT) { 72 | navHostFragment.findNavController().navigate(R.id.action_global_trackingFragment) 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/SplashScreenActivity.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui 2 | 3 | import android.content.Intent 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | 7 | class SplashScreenActivity : AppCompatActivity() { 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | Intent(this, MainActivity::class.java).also { 11 | startActivity(it) 12 | overridePendingTransition( 13 | 0, 0 14 | ) 15 | finish() 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/fragments/CancelTrackingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.fragments 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import androidx.fragment.app.DialogFragment 6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 7 | import com.imnstudios.runningapp.R 8 | 9 | class CancelTrackingDialog : DialogFragment() { 10 | 11 | private var yesListener: (() -> Unit)? = null 12 | 13 | fun setYesListener(listener: () -> Unit) { 14 | yesListener = listener 15 | } 16 | 17 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 18 | return MaterialAlertDialogBuilder(requireContext(), R.style.AlertDialogTheme) 19 | .setTitle("Cancel the Run?") 20 | .setMessage("Are you sure to cancel the current run and delete all its data?") 21 | .setIcon(R.drawable.ic_delete) 22 | .setPositiveButton("Yes") { _, _ -> 23 | yesListener?.let { yes -> 24 | yes() 25 | 26 | } 27 | 28 | } 29 | .setNegativeButton("No") { dialogInterface, _ -> 30 | dialogInterface.cancel() 31 | 32 | } 33 | .create() 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/fragments/LogInFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.fragments 2 | 3 | import android.content.Intent 4 | import android.content.SharedPreferences 5 | import android.os.Bundle 6 | import android.view.View 7 | import androidx.fragment.app.Fragment 8 | import androidx.navigation.fragment.findNavController 9 | import com.google.android.gms.auth.api.signin.GoogleSignIn 10 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount 11 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 12 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions 13 | import com.google.android.gms.common.api.ApiException 14 | import com.google.firebase.auth.GoogleAuthProvider 15 | import com.imnstudios.runningapp.R 16 | import com.imnstudios.runningapp.other.Constants.RC_SIGN_IN 17 | import com.imnstudios.runningapp.other.hide 18 | import com.imnstudios.runningapp.other.show 19 | import com.imnstudios.runningapp.other.snackbar 20 | import com.imnstudios.runningapp.ui.MainActivity.Companion.auth 21 | import dagger.hilt.android.AndroidEntryPoint 22 | import kotlinx.android.synthetic.main.fragment_login.* 23 | import timber.log.Timber 24 | import javax.inject.Inject 25 | 26 | 27 | @AndroidEntryPoint 28 | class LogInFragment : Fragment(R.layout.fragment_login) { 29 | 30 | @Inject 31 | lateinit var sharedPref: SharedPreferences 32 | 33 | lateinit var mGoogleSignInClient: GoogleSignInClient 34 | 35 | 36 | @set:Inject 37 | var isFirstAppOpen = true 38 | 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 40 | super.onViewCreated(view, savedInstanceState) 41 | 42 | val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) 43 | .requestEmail() 44 | .requestIdToken(getString(R.string.default_web_client_id)) 45 | .build() 46 | 47 | mGoogleSignInClient = GoogleSignIn.getClient(requireContext(), gso) 48 | 49 | login_button.setOnClickListener { 50 | login() 51 | } 52 | 53 | } 54 | 55 | override fun onStart() { 56 | super.onStart() 57 | 58 | if (auth.currentUser != null && !isFirstAppOpen) 59 | findNavController().navigate(R.id.action_logInFragment_to_runFragment) 60 | else 61 | login_button.show() 62 | 63 | 64 | } 65 | 66 | private fun login() { 67 | progress_bar.show() 68 | login_button.hide() 69 | val signInIntent = mGoogleSignInClient.signInIntent 70 | startActivityForResult(signInIntent, RC_SIGN_IN) 71 | } 72 | 73 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 74 | super.onActivityResult(requestCode, resultCode, data) 75 | if (requestCode == RC_SIGN_IN) { 76 | 77 | val task = GoogleSignIn.getSignedInAccountFromIntent(data) 78 | try { 79 | val account = task.getResult(ApiException::class.java) 80 | firebaseAuthWithGoogle(account!!) 81 | } catch (e: ApiException) { 82 | base_layout.snackbar("Something is Wrong $e") 83 | progress_bar.hide() 84 | login_button.show() 85 | } 86 | } 87 | } 88 | 89 | private fun firebaseAuthWithGoogle(acct: GoogleSignInAccount) { 90 | Timber.d("firebaseAuthWithGoogle: + ${acct.id!!}") 91 | val credential = GoogleAuthProvider.getCredential(acct.idToken, null) 92 | 93 | auth.signInWithCredential(credential) 94 | .addOnCompleteListener( 95 | requireActivity() 96 | ) { task -> 97 | if (task.isSuccessful) { 98 | Timber.d("signInWithCredential:success") 99 | 100 | findNavController().navigate(R.id.action_logInFragment_to_setupFragment) 101 | 102 | } else { 103 | Timber.d("signInWithCredential:failure ${task.exception}") 104 | base_layout.snackbar("Authentication failed ${task.exception.toString()}") 105 | progress_bar.hide() 106 | login_button.show() 107 | } 108 | } 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/fragments/RunFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.fragments 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.widget.AdapterView 8 | import android.widget.ArrayAdapter 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.viewModels 11 | import androidx.lifecycle.Observer 12 | import androidx.navigation.fragment.findNavController 13 | import androidx.recyclerview.widget.LinearLayoutManager 14 | import com.google.firebase.firestore.CollectionReference 15 | import com.imnstudios.runningapp.R 16 | import com.imnstudios.runningapp.adapters.RunAdapter 17 | import com.imnstudios.runningapp.db.Run 18 | import com.imnstudios.runningapp.other.Constants.REQUEST_CODE_LOCATION_PERMISSION 19 | import com.imnstudios.runningapp.other.SortType 20 | import com.imnstudios.runningapp.other.TrackingUtility 21 | import com.imnstudios.runningapp.ui.MainActivity 22 | import com.imnstudios.runningapp.ui.MainActivity.Companion.auth 23 | import com.imnstudios.runningapp.ui.viewmodels.MainViewModel 24 | import com.imnstudios.runningapp.ui.viewmodels.NetworkViewModel 25 | import dagger.hilt.android.AndroidEntryPoint 26 | import kotlinx.android.synthetic.main.activity_main.* 27 | import kotlinx.android.synthetic.main.fragment_run.* 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.GlobalScope 30 | import kotlinx.coroutines.launch 31 | import kotlinx.coroutines.tasks.await 32 | import pub.devrel.easypermissions.AppSettingsDialog 33 | import pub.devrel.easypermissions.EasyPermissions 34 | 35 | @AndroidEntryPoint 36 | class RunFragment : Fragment(R.layout.fragment_run), EasyPermissions.PermissionCallbacks { 37 | 38 | private val viewModel: MainViewModel by viewModels() 39 | private val networkViewModel: NetworkViewModel by viewModels() 40 | private lateinit var runAdapter: RunAdapter 41 | 42 | companion object{ 43 | 44 | val collectionReference: CollectionReference = 45 | MainActivity.firestoreDb.collection("Users") 46 | .document(auth.currentUser?.uid.toString()) 47 | .collection("Runs") 48 | } 49 | 50 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 51 | super.onViewCreated(view, savedInstanceState) 52 | 53 | 54 | if (auth.currentUser != null) { 55 | syncFromFireStore() 56 | } 57 | 58 | 59 | val toolbarText = "Your Runs" 60 | requireActivity().tvToolbarTitle.text = toolbarText 61 | 62 | requestPermissions() 63 | setupRecyclerView() 64 | 65 | when (viewModel.sortType) { 66 | SortType.DATE -> spFilter.setSelection(0) 67 | SortType.RUNNING_TIME -> spFilter.setSelection(1) 68 | SortType.DISTANCE -> spFilter.setSelection(2) 69 | SortType.AVG_SPEED -> spFilter.setSelection(3) 70 | SortType.CALORIES_BURNED -> spFilter.setSelection(4) 71 | } 72 | 73 | val adapter = ArrayAdapter.createFromResource( 74 | requireActivity(), 75 | R.array.filter_options, 76 | R.layout.custom_spinner 77 | ) // where array_name consists of the items to show in Spinner 78 | adapter.setDropDownViewResource(R.layout.custom_spinner) // where custom-spinner is mycustom xml file. 79 | 80 | spFilter.adapter = adapter 81 | spFilter.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { 82 | override fun onNothingSelected(parent: AdapterView<*>?) {} 83 | 84 | override fun onItemSelected( 85 | adapterView: AdapterView<*>?, 86 | view: View?, 87 | position: Int, 88 | id: Long 89 | ) { 90 | when (position) { 91 | 0 -> viewModel.sortRuns(SortType.DATE) 92 | 1 -> viewModel.sortRuns(SortType.RUNNING_TIME) 93 | 2 -> viewModel.sortRuns(SortType.DISTANCE) 94 | 3 -> viewModel.sortRuns(SortType.AVG_SPEED) 95 | 4 -> viewModel.sortRuns(SortType.CALORIES_BURNED) 96 | } 97 | } 98 | 99 | } 100 | 101 | viewModel.runs.observe(viewLifecycleOwner, Observer { 102 | runAdapter.submitList(it) 103 | }) 104 | fab.setOnClickListener { 105 | findNavController().navigate(R.id.action_runFragment_to_trackingFragment) 106 | } 107 | } 108 | 109 | private fun syncFromFireStore() { 110 | networkViewModel.syncDataToRoomFromFirestore() 111 | } 112 | 113 | private fun setupRecyclerView() = rvRuns.apply { 114 | runAdapter = RunAdapter() 115 | adapter = runAdapter 116 | layoutManager = LinearLayoutManager(requireContext()) 117 | } 118 | 119 | 120 | private fun requestPermissions() { 121 | if (TrackingUtility.hasLocationPermissions(requireContext())) { 122 | return 123 | } 124 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 125 | 126 | EasyPermissions.requestPermissions( 127 | this, 128 | "You need to accept permissions to use this app.", 129 | REQUEST_CODE_LOCATION_PERMISSION, 130 | Manifest.permission.ACCESS_FINE_LOCATION, 131 | Manifest.permission.ACCESS_COARSE_LOCATION 132 | ) 133 | } else { 134 | EasyPermissions.requestPermissions( 135 | this, 136 | "You need to accept permissions to use this app.", 137 | REQUEST_CODE_LOCATION_PERMISSION, 138 | Manifest.permission.ACCESS_FINE_LOCATION, 139 | Manifest.permission.ACCESS_COARSE_LOCATION, 140 | Manifest.permission.ACCESS_BACKGROUND_LOCATION 141 | ) 142 | 143 | } 144 | } 145 | 146 | override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { 147 | if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) { 148 | AppSettingsDialog.Builder(this).build().show() 149 | } else { 150 | requestPermissions() 151 | } 152 | } 153 | 154 | override fun onPermissionsGranted(requestCode: Int, perms: MutableList) {} 155 | 156 | override fun onRequestPermissionsResult( 157 | requestCode: Int, 158 | permissions: Array, 159 | grantResults: IntArray 160 | ) { 161 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 162 | EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) 163 | } 164 | 165 | 166 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/fragments/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.fragments 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.net.ConnectivityManager 6 | import android.net.NetworkCapabilities 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.view.View 10 | import androidx.fragment.app.Fragment 11 | import androidx.fragment.app.viewModels 12 | import androidx.lifecycle.Observer 13 | import androidx.navigation.fragment.findNavController 14 | import com.google.android.material.snackbar.Snackbar 15 | import com.imnstudios.runningapp.R 16 | import com.imnstudios.runningapp.other.Constants 17 | import com.imnstudios.runningapp.other.Constants.KEY_NAME 18 | import com.imnstudios.runningapp.other.Constants.KEY_WEIGHT 19 | import com.imnstudios.runningapp.ui.MainActivity 20 | import com.imnstudios.runningapp.ui.MainActivity.Companion.auth 21 | import com.imnstudios.runningapp.ui.MainActivity.Companion.firestoreDb 22 | import com.imnstudios.runningapp.ui.viewmodels.StatisticsViewModel 23 | import dagger.hilt.android.AndroidEntryPoint 24 | import kotlinx.android.synthetic.main.activity_main.* 25 | import kotlinx.android.synthetic.main.fragment_settings.* 26 | import javax.inject.Inject 27 | 28 | @AndroidEntryPoint 29 | class SettingsFragment : Fragment(R.layout.fragment_settings) { 30 | 31 | @Inject 32 | lateinit var sharedPreferences: SharedPreferences 33 | 34 | private val viewModel: StatisticsViewModel by viewModels() 35 | 36 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 37 | super.onViewCreated(view, savedInstanceState) 38 | 39 | val toolbarText = "Settings" 40 | requireActivity().tvToolbarTitle.text = toolbarText 41 | 42 | loadFieldsFromSharedPref() 43 | btnApplyChanges.setOnClickListener { 44 | val success = applyChangesToSharedPref() 45 | if (success) { 46 | Snackbar.make(view, "Saved changes", Snackbar.LENGTH_LONG).show() 47 | } else { 48 | Snackbar.make(view, "Please fill out all the fields", Snackbar.LENGTH_LONG).show() 49 | } 50 | } 51 | 52 | btnLogOut.setOnClickListener { 53 | logOut() 54 | 55 | } 56 | 57 | btnSync.setOnClickListener { 58 | syncToFirebase() 59 | } 60 | } 61 | 62 | private fun syncToFirebase() { 63 | 64 | if (isInternetAvailable()){ 65 | if (auth.currentUser != null) { 66 | 67 | viewModel.runsSortedByDate.observe(viewLifecycleOwner, Observer { 68 | it?.let { 69 | 70 | for (i in it) { 71 | firestoreDb.collection("Users") 72 | .document(auth.currentUser?.uid.toString()) 73 | .collection("Runs") 74 | .document(i.id) 75 | .set(i) 76 | 77 | } 78 | 79 | } 80 | }) 81 | 82 | 83 | } else { 84 | Snackbar.make(base_layout, "Something's wrong", Snackbar.LENGTH_LONG).show() 85 | } 86 | }else{ 87 | Snackbar.make(base_layout, "No Internet", Snackbar.LENGTH_LONG).show() 88 | } 89 | 90 | } 91 | 92 | private fun logOut() { 93 | 94 | if (isInternetAvailable()){ 95 | auth.signOut() 96 | sharedPreferences.edit() 97 | .putBoolean(Constants.KEY_FIRST_TIME_TOGGLE, true) 98 | .apply() 99 | findNavController().navigate(R.id.action_settingsFragment_to_logInFragment) 100 | }else{ 101 | Snackbar.make(base_layout, "No Internet", Snackbar.LENGTH_LONG).show() 102 | } 103 | 104 | } 105 | 106 | 107 | private fun loadFieldsFromSharedPref() { 108 | // val name = sharedPreferences.getString(KEY_NAME, "") 109 | val weight = sharedPreferences.getFloat(KEY_WEIGHT, 80f) 110 | // etName.setText(name) 111 | etWeight.setText(weight.toInt().toString()) 112 | } 113 | 114 | private fun applyChangesToSharedPref(): Boolean { 115 | // val nameText = etName.text.toString() 116 | val weightText = etWeight.text.toString() 117 | if (weightText.isEmpty()) { 118 | return false 119 | } 120 | sharedPreferences.edit() 121 | // .putString(KEY_NAME, nameText) 122 | .putFloat(KEY_WEIGHT, weightText.toFloat()) 123 | .apply() 124 | // val toolbarText = "Let's go $nameText" 125 | // requireActivity().tvToolbarTitle.text = toolbarText 126 | return true 127 | } 128 | 129 | 130 | private fun isInternetAvailable(): Boolean { 131 | 132 | val connectivityManager = 133 | requireContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 134 | val nw = connectivityManager.activeNetwork ?: return false 135 | val actNw = connectivityManager.getNetworkCapabilities(nw) ?: return false 136 | return when { 137 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true 138 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true 139 | else -> false 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/fragments/SetupFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.fragments 2 | 3 | import android.content.SharedPreferences 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.fragment.app.Fragment 7 | import androidx.navigation.fragment.findNavController 8 | import com.google.android.material.snackbar.Snackbar 9 | import com.imnstudios.runningapp.R 10 | import com.imnstudios.runningapp.other.Constants.KEY_FIRST_TIME_TOGGLE 11 | import com.imnstudios.runningapp.other.Constants.KEY_NAME 12 | import com.imnstudios.runningapp.other.Constants.KEY_WEIGHT 13 | import com.imnstudios.runningapp.ui.MainActivity.Companion.auth 14 | import dagger.hilt.android.AndroidEntryPoint 15 | import kotlinx.android.synthetic.main.fragment_setup.* 16 | import javax.inject.Inject 17 | 18 | @AndroidEntryPoint 19 | class SetupFragment : Fragment(R.layout.fragment_setup) { 20 | 21 | @Inject 22 | lateinit var sharedPref: SharedPreferences 23 | 24 | 25 | private lateinit var userName: String 26 | 27 | // @set:Inject 28 | // var isFirstAppOpen = true 29 | 30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 31 | super.onViewCreated(view, savedInstanceState) 32 | 33 | // if(!isFirstAppOpen) { 34 | // val navOptions = NavOptions.Builder() 35 | // .setPopUpTo(R.id.setupFragment, true) 36 | // .build() 37 | // findNavController().navigate( 38 | // R.id.action_setupFragment_to_runFragment, 39 | // savedInstanceState, 40 | // navOptions 41 | // ) 42 | // } 43 | 44 | userName = auth.currentUser?.displayName.toString() 45 | 46 | val personalizedText = "Welcome!\n$userName" 47 | tvWelcome.text = personalizedText 48 | 49 | tvContinue.setOnClickListener { 50 | val success = writePersonalDataToSharedPref() 51 | if (success) { 52 | findNavController().navigate(R.id.action_setupFragment_to_runFragment) 53 | } else { 54 | Snackbar.make(requireView(), "Please enter all the fields", Snackbar.LENGTH_SHORT) 55 | .show() 56 | } 57 | 58 | } 59 | } 60 | 61 | private fun writePersonalDataToSharedPref(): Boolean { 62 | 63 | val weight = etWeight.text.toString() 64 | if (weight.isEmpty()) { 65 | return false 66 | } 67 | sharedPref.edit() 68 | .putString(KEY_NAME, userName) 69 | .putFloat(KEY_WEIGHT, weight.toFloat()) 70 | .putBoolean(KEY_FIRST_TIME_TOGGLE, false) 71 | .apply() 72 | // val toolbarText = "Let's go, $name!" 73 | // requireActivity().tvToolbarTitle.text = toolbarText 74 | return true 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/fragments/StatisticsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.fragments 2 | 3 | import android.graphics.Color 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.core.content.ContextCompat 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.Observer 10 | import com.github.mikephil.charting.components.XAxis 11 | import com.github.mikephil.charting.data.BarData 12 | import com.github.mikephil.charting.data.BarDataSet 13 | import com.github.mikephil.charting.data.BarEntry 14 | import com.imnstudios.runningapp.R 15 | import com.imnstudios.runningapp.other.CustomMarkerView 16 | import com.imnstudios.runningapp.other.TrackingUtility 17 | import com.imnstudios.runningapp.ui.viewmodels.StatisticsViewModel 18 | import dagger.hilt.android.AndroidEntryPoint 19 | import kotlinx.android.synthetic.main.activity_main.* 20 | import kotlinx.android.synthetic.main.fragment_statistics.* 21 | import kotlin.math.round 22 | 23 | @AndroidEntryPoint 24 | class StatisticsFragment : Fragment(R.layout.fragment_statistics) { 25 | 26 | private val viewModel: StatisticsViewModel by viewModels() 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | val toolbarText = "Statistics" 31 | requireActivity().tvToolbarTitle.text = toolbarText 32 | subscribeToObservers() 33 | setupBarChart() 34 | } 35 | 36 | private fun setupBarChart() { 37 | barChart.xAxis.apply { 38 | position = XAxis.XAxisPosition.BOTTOM 39 | setDrawLabels(false) 40 | axisLineColor = Color.WHITE 41 | textColor = Color.WHITE 42 | setDrawGridLines(false) 43 | } 44 | 45 | barChart.axisLeft.apply { 46 | axisLineColor = Color.WHITE 47 | textColor = Color.WHITE 48 | setDrawGridLines(false) 49 | } 50 | barChart.axisRight.apply { 51 | axisLineColor = Color.WHITE 52 | textColor = Color.WHITE 53 | setDrawGridLines(false) 54 | } 55 | barChart.apply { 56 | description.text = "Average Speed Over Time" 57 | legend.isEnabled = false 58 | } 59 | } 60 | 61 | private fun subscribeToObservers() { 62 | viewModel.totalTimeRun.observe(viewLifecycleOwner, Observer { 63 | it?.let { 64 | val totalTimeRun = TrackingUtility.getFormattedStopWatchTime(it) 65 | tvTotalTime.text = totalTimeRun 66 | } 67 | }) 68 | viewModel.totalDistance.observe(viewLifecycleOwner, Observer { 69 | it?.let { 70 | val km = it / 1000f 71 | val totalDistance = round(km * 10f) / 10f 72 | val totalDistanceString = "${totalDistance}km" 73 | tvTotalDistance.text = totalDistanceString 74 | } 75 | }) 76 | viewModel.totalAvgSpeed.observe(viewLifecycleOwner, Observer { 77 | it?.let { 78 | val avgSpeed = round(it * 10f) / 10f 79 | val avgSpeedString = "${avgSpeed}km/h" 80 | tvAverageSpeed.text = avgSpeedString 81 | } 82 | }) 83 | viewModel.totalCaloriesBurned.observe(viewLifecycleOwner, Observer { 84 | it?.let { 85 | val totalCalories = "${it}kcal" 86 | tvTotalCalories.text = totalCalories 87 | } 88 | }) 89 | 90 | viewModel.runsSortedByDate.observe(viewLifecycleOwner, Observer { 91 | it?.let { 92 | val allAvgSpeeds = 93 | it.indices.map { i -> BarEntry(i.toFloat(), it[i].avgSpeedInKMH) } 94 | val barDataSet = BarDataSet(allAvgSpeeds, "Avg Speed Over Time").apply { 95 | valueTextColor = Color.BLACK 96 | color = ContextCompat.getColor(requireContext(), R.color.white) 97 | } 98 | 99 | barChart.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.colorAccent)) 100 | barChart.data = BarData(barDataSet) 101 | barChart.marker = 102 | CustomMarkerView(it.reversed(), requireContext(), R.layout.marker_view) 103 | barChart.invalidate() 104 | } 105 | }) 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/fragments/TrackingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.fragments 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.view.* 9 | import androidx.annotation.RequiresApi 10 | import androidx.fragment.app.Fragment 11 | import androidx.fragment.app.viewModels 12 | import androidx.lifecycle.Observer 13 | import androidx.navigation.fragment.findNavController 14 | import com.google.android.gms.maps.CameraUpdateFactory 15 | import com.google.android.gms.maps.GoogleMap 16 | import com.google.android.gms.maps.model.LatLngBounds 17 | import com.google.android.gms.maps.model.PolylineOptions 18 | import com.google.android.material.snackbar.Snackbar 19 | import com.imnstudios.runningapp.R 20 | import com.imnstudios.runningapp.db.Run 21 | import com.imnstudios.runningapp.other.Constants.ACTION_PAUSE_SERVICE 22 | import com.imnstudios.runningapp.other.Constants.ACTION_START_OR_RESUME_SERVICE 23 | import com.imnstudios.runningapp.other.Constants.ACTION_STOP_SERVICE 24 | import com.imnstudios.runningapp.other.Constants.MAP_ZOOM 25 | import com.imnstudios.runningapp.other.Constants.POLYLINE_COLOR 26 | import com.imnstudios.runningapp.other.Constants.POLYLINE_WIDTH 27 | import com.imnstudios.runningapp.other.TrackingUtility 28 | import com.imnstudios.runningapp.services.Polyline 29 | import com.imnstudios.runningapp.services.TrackingService 30 | import com.imnstudios.runningapp.ui.viewmodels.MainViewModel 31 | import dagger.hilt.android.AndroidEntryPoint 32 | import kotlinx.android.synthetic.main.activity_main.* 33 | import kotlinx.android.synthetic.main.fragment_tracking.* 34 | import java.io.ByteArrayOutputStream 35 | import java.util.* 36 | import javax.inject.Inject 37 | import kotlin.math.round 38 | 39 | 40 | const val CANCEL_TRACKING_DIALOG_TAG = "CancelDialog" 41 | 42 | @AndroidEntryPoint 43 | class TrackingFragment : Fragment(R.layout.fragment_tracking) { 44 | 45 | private val viewModel: MainViewModel by viewModels() 46 | 47 | private var isTracking = false 48 | private var pathPoints = mutableListOf() 49 | 50 | private var map: GoogleMap? = null 51 | 52 | private var curTimeInMillis = 0L 53 | 54 | private var menu: Menu? = null 55 | 56 | @set:Inject 57 | var weight = 80f 58 | 59 | override fun onCreateView( 60 | inflater: LayoutInflater, 61 | container: ViewGroup?, 62 | savedInstanceState: Bundle? 63 | ): View? { 64 | setHasOptionsMenu(true) 65 | return super.onCreateView(inflater, container, savedInstanceState) 66 | } 67 | 68 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 69 | super.onViewCreated(view, savedInstanceState) 70 | 71 | 72 | val toolbarText = "RUN" 73 | requireActivity().tvToolbarTitle.text = toolbarText 74 | mapView.onCreate(savedInstanceState) 75 | 76 | 77 | btnToggleRun.setOnClickListener { 78 | toggleRun() 79 | } 80 | 81 | if (savedInstanceState != null) { 82 | val cancelTrackingDialog = parentFragmentManager.findFragmentByTag( 83 | CANCEL_TRACKING_DIALOG_TAG 84 | ) as CancelTrackingDialog? 85 | 86 | cancelTrackingDialog?.setYesListener { 87 | stopRun() 88 | } 89 | } 90 | 91 | btnFinishRun.setOnClickListener { 92 | zoomToSeeWholeTrack() 93 | endRunAndSaveToDb() 94 | } 95 | mapView.getMapAsync { 96 | map = it 97 | 98 | addAllPolylines() 99 | } 100 | 101 | subscribeToObservers() 102 | } 103 | 104 | private fun subscribeToObservers() { 105 | TrackingService.isTracking.observe(viewLifecycleOwner, Observer { 106 | updateTracking(it) 107 | }) 108 | TrackingService.pathPoints.observe(viewLifecycleOwner, Observer { 109 | pathPoints = it 110 | addLatestPolyline() 111 | moveCameraToUser() 112 | }) 113 | TrackingService.timeRunInMillis.observe(viewLifecycleOwner, Observer { 114 | curTimeInMillis = it 115 | val formattedTime = TrackingUtility.getFormattedStopWatchTime(curTimeInMillis, true) 116 | tvTimer.text = formattedTime 117 | }) 118 | } 119 | 120 | private fun toggleRun() { 121 | if (isTracking) { 122 | menu?.getItem(0)?.isVisible = true 123 | sendCommandToService(ACTION_PAUSE_SERVICE) 124 | } else { 125 | sendCommandToService(ACTION_START_OR_RESUME_SERVICE) 126 | } 127 | } 128 | 129 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 130 | super.onCreateOptionsMenu(menu, inflater) 131 | inflater.inflate(R.menu.toolbar_tracking_menu, menu) 132 | this.menu = menu 133 | } 134 | 135 | override fun onPrepareOptionsMenu(menu: Menu) { 136 | super.onPrepareOptionsMenu(menu) 137 | if (curTimeInMillis > 0L) { 138 | this.menu?.getItem(0)?.isVisible = true 139 | } 140 | } 141 | 142 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 143 | when (item.itemId) { 144 | R.id.miCancelTracking -> { 145 | showCancelTrackingDialog() 146 | } 147 | } 148 | return super.onOptionsItemSelected(item) 149 | } 150 | 151 | private fun showCancelTrackingDialog() { 152 | CancelTrackingDialog().apply { 153 | setYesListener { 154 | stopRun() 155 | } 156 | }.show(parentFragmentManager, CANCEL_TRACKING_DIALOG_TAG) 157 | } 158 | 159 | private fun stopRun() { 160 | tvTimer.text = "00:00:00:00" 161 | sendCommandToService(ACTION_STOP_SERVICE) 162 | findNavController().navigate(R.id.action_trackingFragment_to_runFragment) 163 | } 164 | 165 | private fun updateTracking(isTracking: Boolean) { 166 | this.isTracking = isTracking 167 | if (!isTracking && curTimeInMillis > 0L) { 168 | btnToggleRun.text = "Start" 169 | btnFinishRun.visibility = View.VISIBLE 170 | } else if (isTracking) { 171 | btnToggleRun.text = "Stop" 172 | menu?.getItem(0)?.isVisible = true 173 | btnFinishRun.visibility = View.GONE 174 | } 175 | } 176 | 177 | private fun moveCameraToUser() { 178 | if (pathPoints.isNotEmpty() && pathPoints.last().isNotEmpty()) { 179 | map?.animateCamera( 180 | CameraUpdateFactory.newLatLngZoom( 181 | pathPoints.last().last(), 182 | MAP_ZOOM 183 | ) 184 | ) 185 | } 186 | } 187 | 188 | private fun zoomToSeeWholeTrack() { 189 | val bounds = LatLngBounds.Builder() 190 | for (polyline in pathPoints) { 191 | for (pos in polyline) { 192 | bounds.include(pos) 193 | } 194 | } 195 | map?.moveCamera( 196 | CameraUpdateFactory.newLatLngBounds( 197 | bounds.build(), 198 | mapView.width, 199 | mapView.height, 200 | (mapView.height * 0.05f).toInt() 201 | ) 202 | ) 203 | } 204 | 205 | @RequiresApi(Build.VERSION_CODES.O) 206 | @SuppressLint("SetWorldReadable") 207 | private fun endRunAndSaveToDb() { 208 | map?.snapshot { bmp -> 209 | var distanceInMeters = 0 210 | for (polyline in pathPoints) { 211 | distanceInMeters += TrackingUtility.calculatePolylineLength(polyline).toInt() 212 | } 213 | val avgSpeed = 214 | round((distanceInMeters / 1000f) / (curTimeInMillis / 1000f / 60 / 60) * 10) / 10f 215 | val dateTimestamp = Calendar.getInstance().timeInMillis 216 | val caloriesBurned = ((distanceInMeters / 1000f) * weight).toInt() 217 | val id = (System.currentTimeMillis() / 1000).toInt().toString() 218 | 219 | //converting bitmap to string 220 | val byteArrayOutputStream = ByteArrayOutputStream() 221 | bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) 222 | val byteArray = byteArrayOutputStream.toByteArray() 223 | val bitmapString = Base64.getEncoder().encodeToString(byteArray) 224 | 225 | val run = 226 | Run( 227 | id, 228 | bitmapString, 229 | dateTimestamp, 230 | avgSpeed, 231 | distanceInMeters, 232 | curTimeInMillis, 233 | caloriesBurned 234 | ) 235 | viewModel.insertRun(run) 236 | Snackbar.make( 237 | requireActivity().findViewById(R.id.rootView), 238 | "Run saved successfully", 239 | Snackbar.LENGTH_LONG 240 | ).show() 241 | stopRun() 242 | } 243 | } 244 | 245 | private fun addAllPolylines() { 246 | for (polyline in pathPoints) { 247 | val polylineOptions = PolylineOptions() 248 | .color(POLYLINE_COLOR) 249 | .width(POLYLINE_WIDTH) 250 | .addAll(polyline) 251 | map?.addPolyline(polylineOptions) 252 | } 253 | } 254 | 255 | private fun addLatestPolyline() { 256 | if (pathPoints.isNotEmpty() && pathPoints.last().size > 1) { 257 | val preLastLatLng = pathPoints.last()[pathPoints.last().size - 2] 258 | val lastLatLng = pathPoints.last().last() 259 | val polylineOptions = PolylineOptions() 260 | .color(POLYLINE_COLOR) 261 | .width(POLYLINE_WIDTH) 262 | .add(preLastLatLng) 263 | .add(lastLatLng) 264 | 265 | map?.addPolyline(polylineOptions) 266 | } 267 | } 268 | 269 | 270 | private fun sendCommandToService(action: String) = 271 | Intent(requireContext(), TrackingService::class.java).also { 272 | it.action = action 273 | requireContext().startService(it) 274 | } 275 | 276 | override fun onStart() { 277 | super.onStart() 278 | mapView?.onStart() 279 | } 280 | 281 | override fun onStop() { 282 | super.onStop() 283 | mapView?.onStop() 284 | } 285 | 286 | override fun onResume() { 287 | super.onResume() 288 | mapView?.onResume() 289 | } 290 | 291 | override fun onPause() { 292 | super.onPause() 293 | mapView?.onPause() 294 | } 295 | 296 | override fun onLowMemory() { 297 | super.onLowMemory() 298 | mapView?.onLowMemory() 299 | } 300 | 301 | override fun onSaveInstanceState(outState: Bundle) { 302 | super.onSaveInstanceState(outState) 303 | mapView?.onSaveInstanceState(outState) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/viewmodels/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.viewmodels 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.MediatorLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.imnstudios.runningapp.db.Run 8 | import com.imnstudios.runningapp.other.SortType 9 | import com.imnstudios.runningapp.repositories.MainRepository 10 | import kotlinx.coroutines.launch 11 | 12 | class MainViewModel @ViewModelInject constructor( 13 | val mainRepository: MainRepository 14 | ) : ViewModel() { 15 | 16 | private val runsSortedByDate = mainRepository.getAllRunsSortedByDate() 17 | private val runsSortedByDistance = mainRepository.getAllRunsSortedByDistance() 18 | private val runsSortedByCaloriesBurned = mainRepository.getAllRunsSortedByCaloriesBurned() 19 | private val runsSortedByTimeInMillis = mainRepository.getAllRunsSortedByTimeInMillis() 20 | private val runsSortedByAvgSpeed = mainRepository.getAllRunsSortedByAvgSpeed() 21 | 22 | val runs = MediatorLiveData>() 23 | var sortType = SortType.DATE 24 | 25 | init { 26 | runs.addSource(runsSortedByDate) { result -> 27 | if (sortType == SortType.DATE) { 28 | result?.let { runs.value = it } 29 | } 30 | } 31 | runs.addSource(runsSortedByAvgSpeed) { result -> 32 | if (sortType == SortType.AVG_SPEED) { 33 | result?.let { runs.value = it } 34 | } 35 | } 36 | runs.addSource(runsSortedByCaloriesBurned) { result -> 37 | if (sortType == SortType.CALORIES_BURNED) { 38 | result?.let { runs.value = it } 39 | } 40 | } 41 | runs.addSource(runsSortedByDistance) { result -> 42 | if (sortType == SortType.DISTANCE) { 43 | result?.let { runs.value = it } 44 | } 45 | } 46 | runs.addSource(runsSortedByTimeInMillis) { result -> 47 | if (sortType == SortType.RUNNING_TIME) { 48 | result?.let { runs.value = it } 49 | } 50 | } 51 | } 52 | 53 | fun sortRuns(sortType: SortType) = when (sortType) { 54 | SortType.DATE -> runsSortedByDate.value?.let { runs.value = it } 55 | SortType.RUNNING_TIME -> runsSortedByTimeInMillis.value?.let { runs.value = it } 56 | SortType.AVG_SPEED -> runsSortedByAvgSpeed.value?.let { runs.value = it } 57 | SortType.DISTANCE -> runsSortedByDistance.value?.let { runs.value = it } 58 | SortType.CALORIES_BURNED -> runsSortedByCaloriesBurned.value?.let { runs.value = it } 59 | }.also { 60 | this.sortType = sortType 61 | } 62 | 63 | fun insertRun(run: Run) = viewModelScope.launch { 64 | mainRepository.insertRun(run) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/viewmodels/NetworkViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.viewmodels 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.imnstudios.runningapp.db.Run 7 | import com.imnstudios.runningapp.repositories.MainRepository 8 | import com.imnstudios.runningapp.ui.fragments.RunFragment.Companion.collectionReference 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.tasks.await 11 | 12 | class NetworkViewModel @ViewModelInject constructor( 13 | private val mainRepository: MainRepository 14 | ) : ViewModel() { 15 | 16 | 17 | fun syncDataToRoomFromFirestore() = viewModelScope.launch { 18 | 19 | val list: MutableList = 20 | collectionReference.get().await().toObjects(Run::class.java) 21 | 22 | for (i in list) 23 | mainRepository.insertRun(i) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imnstudios/runningapp/ui/viewmodels/StatisticsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp.ui.viewmodels 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.ViewModel 5 | import com.imnstudios.runningapp.repositories.MainRepository 6 | 7 | class StatisticsViewModel @ViewModelInject constructor( 8 | val mainRepository: MainRepository 9 | ) : ViewModel() { 10 | 11 | val totalTimeRun = mainRepository.getTotalTimeInMillis() 12 | val totalDistance = mainRepository.getTotalDistance() 13 | val totalCaloriesBurned = mainRepository.getTotalCaloriesBurned() 14 | val totalAvgSpeed = mainRepository.getTotalAvgSpeed() 15 | 16 | val runsSortedByDate = mainRepository.getAllRunsSortedByDate() 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v23/splash_screen_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_nav_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_white.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_directions_run_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_graph.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/7d1a0da914babeba7a1f794dfcb3404dd51237d2/app/src/main/res/drawable/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_run.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_screen_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/view_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/font/poppins.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/7d1a0da914babeba7a1f794dfcb3404dd51237d2/app/src/main/res/font/poppins.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/7d1a0da914babeba7a1f794dfcb3404dd51237d2/app/src/main/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout-land/fragment_statistics.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 32 | 33 | 43 | 44 | 56 | 57 | 70 | 71 | 80 | 81 | 91 | 92 | 105 | 106 | 112 | 113 | 119 | 120 | 128 | 129 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/fragment_tracking.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 31 | 32 | 44 | 45 | 58 | 59 | 60 | 61 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 23 | 24 | 37 | 38 | 39 | 40 | 41 | 49 | 50 | 57 | 58 | 59 | 60 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_spinner.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 17 | 18 | 29 | 30 | 41 | 42 | 43 | 52 | 53 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_run.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 28 | 29 | 38 | 39 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 16 | 17 | 35 | 36 | 44 | 45 |