├── .gitattributes ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── animsh │ │ └── runningtracker │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── animsh │ │ │ └── runningtracker │ │ │ ├── BaseApplication.kt │ │ │ ├── adapters │ │ │ └── RunAdapter.kt │ │ │ ├── db │ │ │ ├── Converters.kt │ │ │ ├── Run.kt │ │ │ ├── RunDAO.kt │ │ │ └── RunningDatabase.kt │ │ │ ├── di │ │ │ ├── AppModule.kt │ │ │ └── ServiceModule.kt │ │ │ ├── other │ │ │ ├── Constants.kt │ │ │ ├── CustomMarkerView.kt │ │ │ ├── SortsType.kt │ │ │ └── TrackingUtility.kt │ │ │ ├── repositories │ │ │ └── MainRepository.kt │ │ │ ├── services │ │ │ └── TrackingService.kt │ │ │ └── ui │ │ │ ├── MainActivity.kt │ │ │ ├── fragments │ │ │ ├── CancelTrackingDialog.kt │ │ │ ├── RunFragment.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── SetupFragment.kt │ │ │ ├── StatisticsFragment.kt │ │ │ └── TrackingFragment.kt │ │ │ └── viewmodels │ │ │ ├── MainViewModel.kt │ │ │ └── StatisticsViewModel.kt │ └── res │ │ ├── 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_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_pause_black_24dp.xml │ │ ├── ic_run.xml │ │ ├── ic_settings.xml │ │ └── ic_stop_black_24dp.xml │ │ ├── layout-land │ │ ├── fragment_statistics.xml │ │ └── fragment_tracking.xml │ │ ├── layout │ │ ├── activity_main.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-v29 │ │ └── styles.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── animsh │ └── runningtracker │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── image1.jpeg ├── image2.jpeg ├── image3.jpeg └── image4.jpeg └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.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/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MVVM Running Tracker App 2 | 3 | Appliation wrriten in kotlin to track your run with dagger-hilt, room databse and google maps api for android. 4 | 5 | ### Some ScreenShots are as follows: 6 | 7 | Your Run Statistics Settings Tracking Run 8 | 9 | ### Please do ⭐ to the repository, if it helped you and feel free to create pull request for any type of changes. 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /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 | 8 | 9 | android { 10 | compileSdkVersion 30 11 | buildToolsVersion "30.0.1" 12 | 13 | defaultConfig { 14 | applicationId "com.animsh.runningtracker" 15 | minSdkVersion 21 16 | targetSdkVersion 30 17 | versionCode 1 18 | versionName "1.0" 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 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = JavaVersion.VERSION_1_8.toString() 36 | } 37 | } 38 | 39 | dependencies { 40 | implementation fileTree(dir: "libs", include: ["*.jar"]) 41 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 42 | implementation 'androidx.core:core-ktx:1.3.1' 43 | implementation 'androidx.appcompat:appcompat:1.2.0' 44 | implementation 'androidx.constraintlayout:constraintlayout:2.0.1' 45 | implementation 'androidx.cardview:cardview:1.0.0' 46 | testImplementation 'junit:junit:4.12' 47 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 48 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 49 | 50 | // Material Design 51 | implementation 'com.google.android.material:material:1.3.0-alpha02' 52 | 53 | // Architectural Components 54 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" 55 | 56 | // Room 57 | implementation "androidx.room:room-runtime:2.2.5" 58 | kapt "androidx.room:room-compiler:2.2.5" 59 | 60 | // Kotlin Extensions and Coroutines support for Room 61 | implementation "androidx.room:room-ktx:2.2.5" 62 | 63 | // Coroutines 64 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5' 65 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5' 66 | 67 | // Coroutine Lifecycle Scopes 68 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" 69 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" 70 | 71 | // Navigation Components 72 | implementation "androidx.navigation:navigation-fragment-ktx:2.3.0" 73 | implementation "androidx.navigation:navigation-ui-ktx:2.3.0" 74 | 75 | // Glide 76 | implementation 'com.github.bumptech.glide:glide:4.11.0' 77 | kapt 'com.github.bumptech.glide:compiler:4.11.0' 78 | 79 | // Google Maps Location Services 80 | implementation 'com.google.android.gms:play-services-location:17.0.0' 81 | implementation 'com.google.android.gms:play-services-maps:17.0.0' 82 | 83 | // Dagger Core 84 | implementation "com.google.dagger:dagger:2.28.1" 85 | kapt "com.google.dagger:dagger-compiler:2.25.2" 86 | 87 | // Dagger Android 88 | api 'com.google.dagger:dagger-android:2.28.1' 89 | api 'com.google.dagger:dagger-android-support:2.28.1' 90 | kapt 'com.google.dagger:dagger-android-processor:2.23.2' 91 | 92 | // Activity KTX for viewModels() 93 | implementation "androidx.activity:activity-ktx:1.1.0" 94 | 95 | // Dagger - Hilt 96 | implementation "com.google.dagger:hilt-android:2.28-alpha" 97 | kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" 98 | 99 | implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02" 100 | kapt "androidx.hilt:hilt-compiler:1.0.0-alpha02" 101 | 102 | // Easy Permissions 103 | implementation 'pub.devrel:easypermissions:3.0.0' 104 | 105 | // Timber 106 | implementation 'com.jakewharton.timber:timber:4.7.1' 107 | 108 | // MPAndroidChart 109 | implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' 110 | 111 | implementation 'android.arch.lifecycle:extensions:1.1.1' 112 | 113 | } -------------------------------------------------------------------------------- /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/animsh/runningtracker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker 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.animsh.runningtracker", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker 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 | override fun onCreate() { 11 | super.onCreate() 12 | Timber.plant(Timber.DebugTree()) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/adapters/RunAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.AsyncListDiffer 7 | import androidx.recyclerview.widget.DiffUtil 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.animsh.runningtracker.R 10 | import com.animsh.runningtracker.db.Run 11 | import com.animsh.runningtracker.other.TrackingUtility 12 | import com.bumptech.glide.Glide 13 | import kotlinx.android.synthetic.main.item_run.view.* 14 | import java.text.SimpleDateFormat 15 | import java.util.* 16 | 17 | class RunAdapter : RecyclerView.Adapter() { 18 | 19 | inner class RunViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) 20 | 21 | val diffCallBack = object : DiffUtil.ItemCallback() { 22 | override fun areItemsTheSame(oldItem: Run, newItem: Run): Boolean { 23 | return oldItem.id == newItem.id 24 | } 25 | 26 | override fun areContentsTheSame(oldItem: Run, newItem: Run): Boolean { 27 | return oldItem.hashCode() == newItem.hashCode() 28 | } 29 | } 30 | 31 | val differ = AsyncListDiffer(this, diffCallBack) 32 | 33 | fun submitList(list: List) = differ.submitList(list) 34 | 35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RunViewHolder { 36 | return RunViewHolder( 37 | LayoutInflater.from(parent.context).inflate( 38 | R.layout.item_run, 39 | parent, 40 | false 41 | ) 42 | ) 43 | } 44 | 45 | override fun onBindViewHolder(holder: RunViewHolder, position: Int) { 46 | val run = differ.currentList[position] 47 | holder.itemView.apply { 48 | Glide.with(this).load(run.img).into(ivRunImage) 49 | 50 | val calendar = Calendar.getInstance().apply { 51 | timeInMillis = run.timestamp 52 | } 53 | val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.getDefault()) 54 | tvDate.text = dateFormat.format(calendar.time) 55 | 56 | val avgSpeed = "Speed: ${run.avgSpeedInKHMH}km/h" 57 | tvAvgSpeed.text = avgSpeed 58 | 59 | val distanceInKM = "Distance: ${run.distanceInMeters / 1000f}km" 60 | tvDistance.text = distanceInKM 61 | 62 | val time = "Time: " + TrackingUtility.getFormattedStopWatchTime(run.timesInMillis) 63 | tvTime.text = time 64 | 65 | val caloriesBurned = "Calories Burned: ${run.caloriesBurned}kcal" 66 | tvCalories.text = caloriesBurned 67 | } 68 | } 69 | 70 | override fun getItemCount(): Int { 71 | return differ.currentList.size 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/db/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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/animsh/runningtracker/db/Run.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.db 2 | 3 | import android.graphics.Bitmap 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | 8 | @Entity(tableName = "running_table") 9 | data class Run( 10 | var img: Bitmap? = null, 11 | var timestamp: Long = 0L, 12 | var avgSpeedInKHMH: Float = 0f, 13 | var distanceInMeters: Int = 0, 14 | var timesInMillis: Long = 0L, 15 | var caloriesBurned: Int = 0 16 | ) { 17 | @PrimaryKey(autoGenerate = true) 18 | var id: Int? = null 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/db/RunDAO.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.db 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.* 5 | 6 | @Dao 7 | interface RunDAO{ 8 | 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 timesInMillis 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 avgSpeedInKHMH 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(timesInMillis) 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(avgSpeedInKHMH) FROM running_table") 40 | fun getTotalAvgSpeed(): LiveData 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/db/RunningDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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 = 1 10 | ) 11 | @TypeConverters(Converters::class) 12 | abstract class RunningDatabase : RoomDatabase() { 13 | 14 | abstract fun getRunDao(): RunDAO 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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.animsh.runningtracker.db.RunningDatabase 8 | import com.animsh.runningtracker.other.Constants.KEY_FIRST_TIME_TOGGLE 9 | import com.animsh.runningtracker.other.Constants.KEY_NAME 10 | import com.animsh.runningtracker.other.Constants.KEY_WEIGHT 11 | import com.animsh.runningtracker.other.Constants.RUNNING_DATABASE_NAME 12 | import com.animsh.runningtracker.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 | ).build() 33 | 34 | @Singleton 35 | @Provides 36 | fun provideEunDao(db: RunningDatabase) = db.getRunDao() 37 | 38 | @Singleton 39 | @Provides 40 | fun provideSharedPreferences(@ApplicationContext app: Context) = 41 | app.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) 42 | 43 | @Singleton 44 | @Provides 45 | fun providesName(sharedPref: SharedPreferences) = sharedPref.getString(KEY_NAME, "") ?: "" 46 | 47 | @Singleton 48 | @Provides 49 | fun providesWeight(sharedPref: SharedPreferences) = sharedPref.getFloat(KEY_WEIGHT, 80f) 50 | 51 | @Singleton 52 | @Provides 53 | fun providesFirstTimeToggle(sharedPref: SharedPreferences) = 54 | sharedPref.getBoolean(KEY_FIRST_TIME_TOGGLE, true) 55 | 56 | 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/di/ServiceModule.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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.animsh.runningtracker.R 8 | import com.animsh.runningtracker.other.Constants 9 | import com.animsh.runningtracker.ui.MainActivity 10 | import com.google.android.gms.location.FusedLocationProviderClient 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 | @ServiceScoped 43 | @Provides 44 | fun provideBaseNotificationBuilder( 45 | @ApplicationContext app: Context, 46 | pendingIntent: PendingIntent 47 | ) = NotificationCompat.Builder(app, Constants.NOTIFICATION_CHANNEL_ID) 48 | .setAutoCancel(false) 49 | .setOngoing(true) 50 | .setSmallIcon(R.drawable.ic_directions_run_black_24dp) 51 | .setContentTitle("Running Tracker") 52 | .setContentText("00:00:00") 53 | .setContentIntent(pendingIntent) 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/other/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.other 2 | 3 | import android.graphics.Color 4 | import com.animsh.runningtracker.services.PolyLines 5 | 6 | object Constants { 7 | 8 | const val RUNNING_DATABASE_NAME = "running_db" 9 | 10 | const val REQUEST_CODE_LOCATION_PERMISSION = 0 11 | 12 | const val ACTION_START_OR_RESUME_SERVICE = "ACTION_START_OR_RESUME_SERVICE" 13 | const val ACTION_PAUSE_SERVICE = "ACTION_PAUSE_SERVICE" 14 | const val ACTION_STOP_SERVICE = "ACTION_STOP_SERVICE" 15 | const val ACTION_SHOW_TRACKING_FRAGMENT = "ACTION_SHOW_TRACKING_FRAGMENT" 16 | 17 | const val TIMER_UPDATE_INTERVAL = 50L 18 | 19 | const val SHARED_PREFERENCES_NAME = "shared_pref" 20 | const val KEY_FIRST_TIME_TOGGLE = "KEY_FIRST_TIME_TOGGLE" 21 | const val KEY_NAME = "KEY_NAME" 22 | const val KEY_WEIGHT = "KEY_WEIGHT" 23 | 24 | const val LOCATION_UPDATE_INTERVAL = 5000L 25 | const val FASTEST_LOCATION_INTERVAL = 2000L 26 | 27 | const val POLYLINE_COLOR = Color.RED 28 | const val POLYLINE_WIDTH = 8f 29 | const val MAP_ZOOM = 15f 30 | 31 | const val NOTIFICATION_CHANNEL_ID = "tracking_channel" 32 | const val NOTIFICATION_CHANNEL_NAME = "Tracking" 33 | const val NOTIFICATION_ID = 1 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/other/CustomMarkerView.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.other 2 | 3 | import android.content.Context 4 | import com.animsh.runningtracker.db.Run 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 kotlinx.android.synthetic.main.marker_view.view.* 10 | import java.text.SimpleDateFormat 11 | import java.util.* 12 | 13 | class CustomMarkerView( 14 | val runs: List, 15 | c: Context, 16 | layoutId: Int 17 | ) : MarkerView(c, layoutId) { 18 | 19 | override fun getOffset(): MPPointF { 20 | return MPPointF(-width / 2f, -height.toFloat()) 21 | } 22 | 23 | override fun refreshContent(e: Entry?, highlight: Highlight?) { 24 | super.refreshContent(e, highlight) 25 | if (e == null) { 26 | return 27 | } 28 | val curRunId = e.x.toInt() 29 | val run = runs[curRunId] 30 | 31 | val calendar = Calendar.getInstance().apply { 32 | timeInMillis = run.timestamp 33 | } 34 | val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.getDefault()) 35 | tvDate.text = dateFormat.format(calendar.time) 36 | 37 | val avgSpeed = "${run.avgSpeedInKHMH}km/h" 38 | tvAvgSpeed.text = avgSpeed 39 | 40 | val distanceInKM = "${run.distanceInMeters / 1000f}" 41 | tvDistance.text = distanceInKM 42 | 43 | tvDuration.text = TrackingUtility.getFormattedStopWatchTime(run.timesInMillis) 44 | 45 | val caloriesBurned = "${run.caloriesBurned}kcal" 46 | tvCaloriesBurned.text = caloriesBurned 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/other/SortsType.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.other 2 | 3 | enum class SortsType { 4 | DATE, RUNNING_TIME, AVG_SPEED, DISTANCE, CALORIES_BURNED 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/other/TrackingUtility.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.other 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.location.Location 6 | import android.os.Build 7 | import com.animsh.runningtracker.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 | 37 | Location.distanceBetween( 38 | pos1.latitude, 39 | pos1.longitude, 40 | pos2.latitude, 41 | pos2.longitude, 42 | result 43 | ) 44 | distance += result[0] 45 | } 46 | return distance 47 | } 48 | 49 | fun getFormattedStopWatchTime(ms: Long, includeMillis: Boolean = false): String { 50 | var milliSeconds = ms 51 | val hours = TimeUnit.MILLISECONDS.toHours(milliSeconds) 52 | milliSeconds -= TimeUnit.HOURS.toMillis(hours) 53 | val minutes = TimeUnit.MILLISECONDS.toMinutes(milliSeconds) 54 | milliSeconds -= TimeUnit.MINUTES.toMillis(minutes) 55 | val seconds = TimeUnit.MILLISECONDS.toSeconds(milliSeconds) 56 | if (!includeMillis) { 57 | return "${if (hours < 10) "0" else ""}$hours:" + 58 | "${if (minutes < 10) "0" else ""}$minutes:" + 59 | "${if (seconds < 10) "0" else ""}$seconds" 60 | } 61 | milliSeconds -= TimeUnit.SECONDS.toMillis(seconds) 62 | milliSeconds /= 10 63 | return "${if (hours < 10) "0" else ""}$hours:" + 64 | "${if (minutes < 10) "0" else ""}$minutes:" + 65 | "${if (seconds < 10) "0" else ""}$seconds:" + 66 | "${if (milliSeconds < 10) "0" else ""}$milliSeconds" 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/repositories/MainRepository.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.repositories 2 | 3 | import com.animsh.runningtracker.db.Run 4 | import com.animsh.runningtracker.db.RunDAO 5 | import javax.inject.Inject 6 | 7 | class MainRepository @Inject constructor( 8 | 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 getTotalTimesInMillis() = runDao.getTotalTimeInMillis() 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/services/TrackingService.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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.animsh.runningtracker.R 20 | import com.animsh.runningtracker.other.Constants.ACTION_PAUSE_SERVICE 21 | import com.animsh.runningtracker.other.Constants.ACTION_START_OR_RESUME_SERVICE 22 | import com.animsh.runningtracker.other.Constants.ACTION_STOP_SERVICE 23 | import com.animsh.runningtracker.other.Constants.FASTEST_LOCATION_INTERVAL 24 | import com.animsh.runningtracker.other.Constants.LOCATION_UPDATE_INTERVAL 25 | import com.animsh.runningtracker.other.Constants.NOTIFICATION_CHANNEL_ID 26 | import com.animsh.runningtracker.other.Constants.NOTIFICATION_CHANNEL_NAME 27 | import com.animsh.runningtracker.other.Constants.NOTIFICATION_ID 28 | import com.animsh.runningtracker.other.Constants.TIMER_UPDATE_INTERVAL 29 | import com.animsh.runningtracker.other.TrackingUtility 30 | import com.google.android.gms.location.FusedLocationProviderClient 31 | import com.google.android.gms.location.LocationCallback 32 | import com.google.android.gms.location.LocationRequest 33 | import com.google.android.gms.location.LocationRequest.PRIORITY_HIGH_ACCURACY 34 | import com.google.android.gms.location.LocationResult 35 | import com.google.android.gms.maps.model.LatLng 36 | import dagger.hilt.android.AndroidEntryPoint 37 | import kotlinx.coroutines.CoroutineScope 38 | import kotlinx.coroutines.Dispatchers 39 | import kotlinx.coroutines.delay 40 | import kotlinx.coroutines.launch 41 | import timber.log.Timber 42 | import javax.inject.Inject 43 | 44 | typealias PolyLine = MutableList 45 | typealias PolyLines = MutableList 46 | 47 | @AndroidEntryPoint 48 | class TrackingService : LifecycleService() { 49 | 50 | var isFirstRun = true 51 | var serviceKilled = false 52 | 53 | @Inject 54 | lateinit var fusedLocationProviderClient: FusedLocationProviderClient 55 | 56 | private val timeRunInSeconds = MutableLiveData() 57 | 58 | @Inject 59 | lateinit var baseNotificationBuilder: NotificationCompat.Builder 60 | 61 | lateinit var curNotificationBuilder: NotificationCompat.Builder 62 | 63 | companion object { 64 | val isTracking = MutableLiveData() 65 | val pathPoints = MutableLiveData() 66 | val timeRunInMillis = MutableLiveData() 67 | } 68 | 69 | private fun postInitialValues() { 70 | isTracking.postValue(false) 71 | pathPoints.postValue(mutableListOf()) 72 | timeRunInSeconds.postValue(0L) 73 | timeRunInMillis.postValue(0L) 74 | } 75 | 76 | override fun onCreate() { 77 | super.onCreate() 78 | curNotificationBuilder = baseNotificationBuilder 79 | postInitialValues() 80 | fusedLocationProviderClient = FusedLocationProviderClient(this) 81 | 82 | isTracking.observe(this, Observer { 83 | updateLocationTracking(it) 84 | updateNotificationTrackingState(it) 85 | }) 86 | } 87 | 88 | private fun killService() { 89 | serviceKilled = true 90 | isFirstRun = true 91 | pauseService() 92 | postInitialValues() 93 | stopForeground(true) 94 | stopSelf() 95 | } 96 | 97 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 98 | intent?.let { 99 | when (it.action) { 100 | ACTION_START_OR_RESUME_SERVICE -> { 101 | if (isFirstRun) { 102 | startForegroundService() 103 | isFirstRun = false 104 | } else { 105 | Timber.d("Resuming Service") 106 | startTimer() 107 | } 108 | 109 | } 110 | ACTION_PAUSE_SERVICE -> { 111 | pauseService() 112 | Timber.d("Paused Service") 113 | } 114 | ACTION_STOP_SERVICE -> { 115 | killService() 116 | Timber.d("Stopped Service") 117 | } 118 | } 119 | } 120 | return super.onStartCommand(intent, flags, startId) 121 | } 122 | 123 | private var isTimerEnabled = false 124 | private var lapTime = 0L 125 | private var timeRun = 0L 126 | private var timeStarted = 0L 127 | private var lastSecondTimeStamp = 0L 128 | 129 | private fun startTimer() { 130 | addEmptyPolyLine() 131 | isTracking.postValue(true) 132 | timeStarted = System.currentTimeMillis() 133 | isTimerEnabled = true 134 | CoroutineScope(Dispatchers.Main).launch { 135 | while (isTracking.value!!) { 136 | // Time difference between time now and time started 137 | lapTime = System.currentTimeMillis() - timeStarted 138 | // Post new lapTime 139 | timeRunInMillis.postValue(timeRun + lapTime) 140 | 141 | if (timeRunInMillis.value!! >= lastSecondTimeStamp + 1000L) { 142 | timeRunInSeconds.postValue(timeRunInSeconds.value!! + 1) 143 | lastSecondTimeStamp += 1000L 144 | } 145 | delay(TIMER_UPDATE_INTERVAL) 146 | } 147 | timeRun += lapTime 148 | } 149 | } 150 | 151 | private fun pauseService() { 152 | isTracking.postValue(false) 153 | isTimerEnabled = false 154 | } 155 | 156 | private fun updateNotificationTrackingState(isTracking: Boolean) { 157 | val notificationActionText = if (isTracking) "Pause" else "Resume" 158 | val pendingIntent = if (isTracking) { 159 | val pauseIntent = Intent(this, TrackingService::class.java).apply { 160 | action = ACTION_PAUSE_SERVICE 161 | } 162 | PendingIntent.getService(this, 1, pauseIntent, FLAG_UPDATE_CURRENT) 163 | } else { 164 | val resumeIntent = Intent(this, TrackingService::class.java).apply { 165 | action = ACTION_START_OR_RESUME_SERVICE 166 | } 167 | PendingIntent.getService(this, 2, resumeIntent, FLAG_UPDATE_CURRENT) 168 | } 169 | 170 | val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 171 | 172 | curNotificationBuilder.javaClass.getDeclaredField("mActions").apply { 173 | isAccessible = true 174 | set(curNotificationBuilder, ArrayList()) 175 | } 176 | if (!serviceKilled) { 177 | curNotificationBuilder = baseNotificationBuilder 178 | .addAction(R.drawable.ic_pause_black_24dp, notificationActionText, pendingIntent) 179 | notificationManager.notify(NOTIFICATION_ID, curNotificationBuilder.build()) 180 | } 181 | } 182 | 183 | @SuppressLint("MissingPermission") 184 | private fun updateLocationTracking(isTracking: Boolean) { 185 | if (isTracking) { 186 | if (TrackingUtility.hasLocationPermissions(this)) { 187 | val request = LocationRequest().apply { 188 | interval = LOCATION_UPDATE_INTERVAL 189 | fastestInterval = FASTEST_LOCATION_INTERVAL 190 | priority = PRIORITY_HIGH_ACCURACY 191 | } 192 | fusedLocationProviderClient.requestLocationUpdates( 193 | request, 194 | locationCallBack, 195 | Looper.getMainLooper() 196 | ) 197 | } 198 | } else { 199 | fusedLocationProviderClient.removeLocationUpdates(locationCallBack) 200 | } 201 | } 202 | 203 | 204 | private val locationCallBack = object : LocationCallback() { 205 | override fun onLocationResult(result: LocationResult?) { 206 | super.onLocationResult(result) 207 | if (isTracking.value!!) { 208 | result?.locations?.let { locations -> 209 | for (location in locations) { 210 | addPathPoint(location) 211 | Timber.d("NEW LOCATION: ${location.latitude}, ${location.longitude}") 212 | } 213 | } 214 | } 215 | } 216 | } 217 | 218 | private fun addPathPoint(location: Location?) { 219 | location?.let { 220 | val pos = LatLng(location.latitude, location.longitude) 221 | pathPoints.value?.apply { 222 | last().add(pos) 223 | pathPoints.postValue(this) 224 | } 225 | } 226 | 227 | } 228 | 229 | private fun addEmptyPolyLine() = pathPoints.value?.apply { 230 | add(mutableListOf()) 231 | pathPoints.postValue(this) 232 | } ?: pathPoints.postValue(mutableListOf(mutableListOf())) 233 | 234 | private fun startForegroundService() { 235 | startTimer() 236 | isTracking.postValue(true) 237 | 238 | val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) 239 | as NotificationManager 240 | 241 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 242 | createNotificationChannel(notificationManager) 243 | } 244 | 245 | startForeground(NOTIFICATION_ID, baseNotificationBuilder.build()) 246 | 247 | timeRunInSeconds.observe(this, Observer { 248 | if (!serviceKilled) { 249 | val notification = curNotificationBuilder 250 | .setContentText(TrackingUtility.getFormattedStopWatchTime(it * 1000)) 251 | notificationManager.notify(NOTIFICATION_ID, notification.build()) 252 | } 253 | }) 254 | } 255 | 256 | 257 | @RequiresApi(Build.VERSION_CODES.O) 258 | private fun createNotificationChannel(notificationManager: NotificationManager) { 259 | val channel = NotificationChannel( 260 | NOTIFICATION_CHANNEL_ID, 261 | NOTIFICATION_CHANNEL_NAME, 262 | IMPORTANCE_LOW 263 | ) 264 | notificationManager.createNotificationChannel(channel) 265 | } 266 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.ui 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.navigation.fragment.findNavController 8 | import androidx.navigation.ui.setupWithNavController 9 | import com.animsh.runningtracker.R 10 | import com.animsh.runningtracker.other.Constants.ACTION_SHOW_TRACKING_FRAGMENT 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import kotlinx.android.synthetic.main.activity_main.* 13 | 14 | @AndroidEntryPoint 15 | class MainActivity : AppCompatActivity() { 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_main) 20 | 21 | navigationToTrackingFragmentIfNeeded(intent) 22 | 23 | setSupportActionBar(toolbar) 24 | bottomNavigationView.setupWithNavController(navHostFragment.findNavController()) 25 | bottomNavigationView.setOnNavigationItemReselectedListener { 26 | // No Operation 27 | } 28 | navHostFragment.findNavController() 29 | .addOnDestinationChangedListener {_, destination, _ -> 30 | when(destination.id){ 31 | R.id.settingsFragment, R.id.runFragment, R.id.statisticsFragment -> 32 | bottomNavigationView.visibility = View.VISIBLE 33 | else -> bottomNavigationView.visibility = View.GONE 34 | } 35 | } 36 | } 37 | 38 | override fun onNewIntent(intent: Intent?) { 39 | super.onNewIntent(intent) 40 | navigationToTrackingFragmentIfNeeded(intent) 41 | } 42 | 43 | private fun navigationToTrackingFragmentIfNeeded(intent: Intent?){ 44 | if(intent?.action == ACTION_SHOW_TRACKING_FRAGMENT){ 45 | navHostFragment.findNavController().navigate(R.id.action_global_tracking_fragment) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/fragments/CancelTrackingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.ui.fragments 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import androidx.fragment.app.DialogFragment 6 | import com.animsh.runningtracker.R 7 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 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 tracking?") 20 | .setMessage("Are you sure to cancel the current run and delete its all data?") 21 | .setIcon(R.drawable.ic_delete) 22 | .setPositiveButton("Yes") { _, _ -> 23 | yesListener?.let {yes -> 24 | yes() 25 | } 26 | } 27 | .setNegativeButton("No") { dialogInterface, _ -> 28 | dialogInterface.cancel() 29 | } 30 | .create() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/fragments/RunFragment.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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.LinearLayout 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.FragmentPagerAdapter 11 | import androidx.fragment.app.viewModels 12 | import androidx.lifecycle.Observer 13 | import androidx.navigation.fragment.findNavController 14 | import androidx.recyclerview.widget.LinearLayoutManager 15 | import com.animsh.runningtracker.R 16 | import com.animsh.runningtracker.adapters.RunAdapter 17 | import com.animsh.runningtracker.other.Constants.REQUEST_CODE_LOCATION_PERMISSION 18 | import com.animsh.runningtracker.other.SortsType 19 | import com.animsh.runningtracker.other.TrackingUtility 20 | import com.animsh.runningtracker.ui.viewmodels.MainViewModel 21 | import dagger.hilt.android.AndroidEntryPoint 22 | import kotlinx.android.synthetic.main.fragment_run.* 23 | import pub.devrel.easypermissions.AppSettingsDialog 24 | import pub.devrel.easypermissions.EasyPermissions 25 | 26 | @AndroidEntryPoint 27 | class RunFragment : Fragment(R.layout.fragment_run), EasyPermissions.PermissionCallbacks { 28 | 29 | private val viewModel: MainViewModel by viewModels() 30 | 31 | private lateinit var runAdapter: RunAdapter 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | requestPermission() 36 | setupRecyclerView() 37 | 38 | when(viewModel.sortType){ 39 | SortsType.DATE -> spFilter.setSelection(0) 40 | SortsType.RUNNING_TIME -> spFilter.setSelection(1) 41 | SortsType.DISTANCE -> spFilter.setSelection(2) 42 | SortsType.AVG_SPEED -> spFilter.setSelection(3) 43 | SortsType.CALORIES_BURNED -> spFilter.setSelection(4) 44 | } 45 | 46 | spFilter.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{ 47 | override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, pos: Int, id: Long) { 48 | when(pos){ 49 | 0 -> viewModel.sortRuns(SortsType.DATE) 50 | 1 -> viewModel.sortRuns(SortsType.RUNNING_TIME) 51 | 2 -> viewModel.sortRuns(SortsType.DISTANCE) 52 | 3 -> viewModel.sortRuns(SortsType.AVG_SPEED) 53 | 4 -> viewModel.sortRuns(SortsType.CALORIES_BURNED) 54 | } 55 | } 56 | 57 | override fun onNothingSelected(p0: AdapterView<*>?) { 58 | } 59 | } 60 | 61 | viewModel.runs.observe(viewLifecycleOwner, Observer { 62 | runAdapter.submitList(it) 63 | }) 64 | 65 | fab.setOnClickListener { 66 | findNavController().navigate(R.id.action_runFragment_to_trackingFragment2) 67 | } 68 | } 69 | 70 | private fun setupRecyclerView() = rvRuns.apply { 71 | runAdapter = RunAdapter() 72 | adapter = runAdapter 73 | layoutManager = LinearLayoutManager(requireContext()) 74 | } 75 | 76 | private fun requestPermission() { 77 | if (TrackingUtility.hasLocationPermissions(requireContext())) { 78 | return 79 | } 80 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 81 | EasyPermissions.requestPermissions( 82 | this, 83 | "You need to accept location permission to use the app.", 84 | REQUEST_CODE_LOCATION_PERMISSION, 85 | Manifest.permission.ACCESS_COARSE_LOCATION, 86 | Manifest.permission.ACCESS_FINE_LOCATION 87 | ) 88 | } else { 89 | EasyPermissions.requestPermissions( 90 | this, 91 | "You need to accept location permission to use the app.", 92 | REQUEST_CODE_LOCATION_PERMISSION, 93 | Manifest.permission.ACCESS_COARSE_LOCATION, 94 | Manifest.permission.ACCESS_FINE_LOCATION, 95 | Manifest.permission.ACCESS_BACKGROUND_LOCATION 96 | ) 97 | } 98 | } 99 | 100 | override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { 101 | if(EasyPermissions.somePermissionPermanentlyDenied(this,perms)){ 102 | AppSettingsDialog.Builder(this).build().show() 103 | } else { 104 | requestPermission() 105 | } 106 | } 107 | 108 | override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { 109 | } 110 | 111 | override fun onRequestPermissionsResult( 112 | requestCode: Int, 113 | permissions: Array, 114 | grantResults: IntArray 115 | ) { 116 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 117 | EasyPermissions.onRequestPermissionsResult(requestCode,permissions,grantResults,this) 118 | } 119 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/fragments/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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 com.animsh.runningtracker.R 8 | import com.animsh.runningtracker.other.Constants.KEY_NAME 9 | import com.animsh.runningtracker.other.Constants.KEY_WEIGHT 10 | import com.google.android.material.snackbar.Snackbar 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import kotlinx.android.synthetic.main.activity_main.* 13 | import kotlinx.android.synthetic.main.fragment_settings.* 14 | import javax.inject.Inject 15 | 16 | @AndroidEntryPoint 17 | class SettingsFragment : Fragment(R.layout.fragment_settings) { 18 | 19 | @Inject 20 | lateinit var sharedPreferences: SharedPreferences 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | loadFieldsFromSharedPrefs() 25 | btnApplyChanges.setOnClickListener { 26 | val success = applyChangesToSharedPref() 27 | if (success) { 28 | Snackbar.make(view, "Saved Changes!!", Snackbar.LENGTH_LONG).show() 29 | } else { 30 | Snackbar.make(view, "Please fill out all the fields", Snackbar.LENGTH_LONG).show() 31 | } 32 | } 33 | } 34 | 35 | private fun loadFieldsFromSharedPrefs() { 36 | val name = sharedPreferences.getString(KEY_NAME, " ") 37 | val weight = sharedPreferences.getFloat(KEY_WEIGHT, 80f) 38 | etName.setText(name) 39 | etWeight.setText(weight.toString()) 40 | } 41 | 42 | private fun applyChangesToSharedPref(): Boolean { 43 | val nameText = etName.text.toString() 44 | val weightText = etWeight.text.toString() 45 | if (nameText.isEmpty() || weightText.isEmpty()) { 46 | return false 47 | } 48 | sharedPreferences.edit() 49 | .putString(KEY_NAME, nameText) 50 | .putFloat(KEY_WEIGHT, weightText.toFloat()) 51 | .apply() 52 | 53 | val toolBarText = "Let's Go, $nameText!" 54 | requireActivity().tvToolbarTitle.text = toolBarText 55 | return true 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/fragments/SetupFragment.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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.NavOptions 8 | import androidx.navigation.fragment.findNavController 9 | import com.animsh.runningtracker.R 10 | import com.animsh.runningtracker.other.Constants.KEY_FIRST_TIME_TOGGLE 11 | import com.animsh.runningtracker.other.Constants.KEY_NAME 12 | import com.animsh.runningtracker.other.Constants.KEY_WEIGHT 13 | import com.google.android.material.snackbar.Snackbar 14 | import dagger.hilt.android.AndroidEntryPoint 15 | import kotlinx.android.synthetic.main.activity_main.* 16 | import kotlinx.android.synthetic.main.fragment_setup.* 17 | import javax.inject.Inject 18 | 19 | @AndroidEntryPoint 20 | class SetupFragment : Fragment(R.layout.fragment_setup) { 21 | 22 | @Inject 23 | lateinit var sharedPref: SharedPreferences 24 | 25 | @set:Inject 26 | var isFirstAppOpen = true 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | 31 | if (!isFirstAppOpen) { 32 | val navOption = NavOptions.Builder() 33 | .setPopUpTo(R.id.setupFragment, true) 34 | .build() 35 | findNavController().navigate( 36 | R.id.action_setupFragment_to_runFragment, 37 | savedInstanceState, 38 | navOption 39 | ) 40 | } 41 | 42 | tvContinue.setOnClickListener { 43 | val success = writePersonalDataToSharedPref() 44 | if (success) { 45 | findNavController().navigate(R.id.action_setupFragment_to_runFragment) 46 | } else { 47 | Snackbar.make( 48 | requireView(), 49 | "Please enter all the details!!", 50 | Snackbar.LENGTH_SHORT 51 | ).show() 52 | } 53 | } 54 | } 55 | 56 | private fun writePersonalDataToSharedPref(): Boolean { 57 | val name = etName.text.toString() 58 | val weight = etWeight.text.toString() 59 | if (name.isEmpty() || weight.isEmpty()) { 60 | return false 61 | } 62 | sharedPref.edit() 63 | .putString(KEY_NAME, name) 64 | .putFloat(KEY_WEIGHT, weight.toFloat()) 65 | .putBoolean(KEY_FIRST_TIME_TOGGLE, false) 66 | .apply() 67 | 68 | val toolBarText = "Let's Go, $name!" 69 | requireActivity().tvToolbarTitle.text = toolBarText 70 | return true 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/fragments/StatisticsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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.animsh.runningtracker.R 11 | import com.animsh.runningtracker.other.CustomMarkerView 12 | import com.animsh.runningtracker.other.TrackingUtility 13 | import com.animsh.runningtracker.ui.viewmodels.StatisticsViewModel 14 | import com.github.mikephil.charting.components.XAxis 15 | import com.github.mikephil.charting.data.BarData 16 | import com.github.mikephil.charting.data.BarDataSet 17 | import com.github.mikephil.charting.data.BarEntry 18 | import dagger.hilt.android.AndroidEntryPoint 19 | import kotlinx.android.synthetic.main.fragment_statistics.* 20 | import kotlin.math.round 21 | 22 | @AndroidEntryPoint 23 | class StatisticsFragment : Fragment(R.layout.fragment_statistics) { 24 | private val viewModel: StatisticsViewModel by viewModels() 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | subscribeToObserver() 29 | setupBarChart() 30 | } 31 | 32 | private fun setupBarChart() { 33 | barChart.xAxis.apply { 34 | position = XAxis.XAxisPosition.BOTTOM 35 | setDrawLabels(false) 36 | axisLineColor = Color.WHITE 37 | textColor = Color.WHITE 38 | setDrawGridLines(false) 39 | } 40 | barChart.axisLeft.apply { 41 | axisLineColor = Color.WHITE 42 | textColor = Color.WHITE 43 | setDrawGridLines(false) 44 | } 45 | barChart.axisRight.apply { 46 | axisLineColor = Color.WHITE 47 | textColor = Color.WHITE 48 | setDrawGridLines(false) 49 | } 50 | barChart.apply { 51 | description.text = "Avg speed ove time" 52 | legend.isEnabled = false 53 | } 54 | } 55 | 56 | private fun subscribeToObserver() { 57 | viewModel.totalTimeRun.observe(viewLifecycleOwner, Observer { 58 | it?.let { 59 | val totalTimeRun = TrackingUtility.getFormattedStopWatchTime(it) 60 | tvTotalTime.text = totalTimeRun 61 | } 62 | }) 63 | viewModel.totalDistance.observe(viewLifecycleOwner, Observer { 64 | it?.let { 65 | val km = it / 1000f 66 | val totalDistance = round(km * 10f) / 10f 67 | val totalDistanceString = "${totalDistance}km" 68 | tvTotalDistance.text = totalDistanceString 69 | } 70 | }) 71 | viewModel.totalAvgSpeed.observe(viewLifecycleOwner, Observer { 72 | it?.let { 73 | val avgSpeed = round(it * 10f) / 10f 74 | val avgSpeedString = "${avgSpeed}km/h" 75 | tvAverageSpeed.text = avgSpeedString 76 | } 77 | }) 78 | viewModel.totalCaloriesBurned.observe(viewLifecycleOwner, Observer { 79 | it?.let { 80 | val totalCalories = "${it}kcal" 81 | tvTotalCalories.text = totalCalories 82 | } 83 | }) 84 | viewModel.runsSortedByDate.observe(viewLifecycleOwner, Observer { 85 | it?.let { 86 | val allAbgSpeed = 87 | it.indices.map { i -> BarEntry(i.toFloat(), it[i].avgSpeedInKHMH) } 88 | val barDataSet = BarDataSet(allAbgSpeed, "Abg Speed Over Time").apply { 89 | valueTextColor = Color.WHITE 90 | color = ContextCompat.getColor(requireContext(), R.color.colorAccent) 91 | } 92 | barChart.data = BarData(barDataSet) 93 | barChart.marker = CustomMarkerView(it, requireContext(), R.layout.marker_view) 94 | barChart.invalidate() 95 | } 96 | }) 97 | } 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/fragments/TrackingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.ui.fragments 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.* 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.viewModels 8 | import androidx.lifecycle.Observer 9 | import androidx.navigation.fragment.findNavController 10 | import com.animsh.runningtracker.R 11 | import com.animsh.runningtracker.db.Run 12 | import com.animsh.runningtracker.other.Constants.ACTION_PAUSE_SERVICE 13 | import com.animsh.runningtracker.other.Constants.ACTION_START_OR_RESUME_SERVICE 14 | import com.animsh.runningtracker.other.Constants.ACTION_STOP_SERVICE 15 | import com.animsh.runningtracker.other.Constants.MAP_ZOOM 16 | import com.animsh.runningtracker.other.Constants.POLYLINE_COLOR 17 | import com.animsh.runningtracker.other.Constants.POLYLINE_WIDTH 18 | import com.animsh.runningtracker.other.TrackingUtility 19 | import com.animsh.runningtracker.services.PolyLine 20 | import com.animsh.runningtracker.services.TrackingService 21 | import com.animsh.runningtracker.ui.viewmodels.MainViewModel 22 | import com.google.android.gms.maps.CameraUpdateFactory 23 | import com.google.android.gms.maps.GoogleMap 24 | import com.google.android.gms.maps.model.LatLngBounds 25 | import com.google.android.gms.maps.model.PolylineOptions 26 | import com.google.android.material.snackbar.Snackbar 27 | import dagger.hilt.android.AndroidEntryPoint 28 | import kotlinx.android.synthetic.main.fragment_tracking.* 29 | import java.util.* 30 | import javax.inject.Inject 31 | import kotlin.math.round 32 | 33 | const val CANCEL_TRACKING_DIALOG_TAG = "CancelDialog" 34 | 35 | @AndroidEntryPoint 36 | class TrackingFragment : Fragment(R.layout.fragment_tracking) { 37 | 38 | private val viewModel: MainViewModel by viewModels() 39 | 40 | private var isTracking = false 41 | private var pathPoints = mutableListOf() 42 | 43 | private var map: GoogleMap? = null 44 | 45 | private var currentTimeInMillis = 0L 46 | 47 | private var menu: Menu? = null 48 | 49 | @set:Inject 50 | var weight = 80f 51 | 52 | override fun onCreateView( 53 | inflater: LayoutInflater, 54 | container: ViewGroup?, 55 | savedInstanceState: Bundle? 56 | ): View? { 57 | setHasOptionsMenu(true) 58 | return super.onCreateView(inflater, container, savedInstanceState) 59 | } 60 | 61 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 62 | super.onViewCreated(view, savedInstanceState) 63 | mapView.onCreate(savedInstanceState) 64 | btnToggleRun.setOnClickListener { 65 | toggleRun() 66 | } 67 | 68 | if (savedInstanceState != null) { 69 | val cancelTrackingDialog = parentFragmentManager.findFragmentByTag( 70 | CANCEL_TRACKING_DIALOG_TAG) as CancelTrackingDialog? 71 | cancelTrackingDialog?.setYesListener { 72 | stopRun() 73 | } 74 | } 75 | 76 | btnFinishRun.setOnClickListener { 77 | zoomToSeeWholeTrack() 78 | endRunAndSaveToDb() 79 | } 80 | 81 | mapView.getMapAsync { 82 | map = it 83 | addAllPolylines() 84 | } 85 | 86 | subscribeToObservers() 87 | } 88 | 89 | private fun subscribeToObservers() { 90 | TrackingService.isTracking.observe(viewLifecycleOwner, Observer { 91 | updateTracking(it) 92 | }) 93 | 94 | TrackingService.pathPoints.observe(viewLifecycleOwner, Observer { 95 | pathPoints = it 96 | addLatestPolyline() 97 | moveCameraToUser() 98 | }) 99 | 100 | TrackingService.timeRunInMillis.observe(viewLifecycleOwner, Observer { 101 | currentTimeInMillis = it 102 | val formattedTime = TrackingUtility.getFormattedStopWatchTime(currentTimeInMillis, true) 103 | tvTimer.text = formattedTime 104 | }) 105 | } 106 | 107 | private fun toggleRun() { 108 | if (isTracking) { 109 | menu?.getItem(0)?.isVisible = true 110 | sendCommandToService(ACTION_PAUSE_SERVICE) 111 | } else { 112 | sendCommandToService(ACTION_START_OR_RESUME_SERVICE) 113 | } 114 | 115 | } 116 | 117 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 118 | super.onCreateOptionsMenu(menu, inflater) 119 | inflater.inflate(R.menu.toolbar_tracking_menu, menu) 120 | this.menu = menu 121 | } 122 | 123 | override fun onPrepareOptionsMenu(menu: Menu) { 124 | super.onPrepareOptionsMenu(menu) 125 | if (currentTimeInMillis > 0L) { 126 | this.menu?.getItem(0)?.isVisible = true 127 | } 128 | } 129 | 130 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 131 | when (item.itemId) { 132 | R.id.miCancelTracking -> 133 | showCancelTrackingDialog() 134 | } 135 | return super.onOptionsItemSelected(item) 136 | } 137 | 138 | private fun showCancelTrackingDialog() { 139 | CancelTrackingDialog().apply { 140 | setYesListener { 141 | stopRun() 142 | } 143 | }.show(parentFragmentManager, CANCEL_TRACKING_DIALOG_TAG) 144 | } 145 | 146 | private fun stopRun() { 147 | tvTimer.text = "00:00:00:00" 148 | sendCommandToService(ACTION_STOP_SERVICE) 149 | findNavController().navigate(R.id.action_trackingFragment_to_runFragment2) 150 | } 151 | 152 | private fun updateTracking(isTracking: Boolean) { 153 | this.isTracking = isTracking 154 | if (!isTracking && currentTimeInMillis > 0L) { 155 | btnToggleRun.text = "Start" 156 | btnFinishRun.visibility = View.VISIBLE 157 | } else if (isTracking) { 158 | btnToggleRun.text = "Stop" 159 | menu?.getItem(0)?.isVisible = true 160 | btnFinishRun.visibility = View.GONE 161 | } 162 | } 163 | 164 | private fun moveCameraToUser() { 165 | if (pathPoints.isNotEmpty() && pathPoints.last().isNotEmpty()) { 166 | map?.animateCamera( 167 | CameraUpdateFactory.newLatLngZoom( 168 | pathPoints.last().last(), 169 | MAP_ZOOM 170 | ) 171 | ) 172 | } 173 | } 174 | 175 | private fun zoomToSeeWholeTrack() { 176 | val bounds = LatLngBounds.Builder() 177 | for (poluline in pathPoints) { 178 | for (pos in poluline) { 179 | bounds.include(pos) 180 | } 181 | } 182 | 183 | map?.moveCamera( 184 | CameraUpdateFactory.newLatLngBounds( 185 | bounds.build(), 186 | mapView.width, 187 | mapView.height, 188 | (mapView.height * 0.05f).toInt() 189 | ) 190 | ) 191 | } 192 | 193 | private fun endRunAndSaveToDb() { 194 | map?.snapshot { bmp -> 195 | var distanceInMeters = 0 196 | for (polyLine in pathPoints) { 197 | distanceInMeters += TrackingUtility.calculatePolyLineLength(polyLine).toInt() 198 | } 199 | val avgSpeed = 200 | round((distanceInMeters / 1000f) / (currentTimeInMillis / 1000f / 60 / 60) * 10) / 10f 201 | val dateTimeStamp = Calendar.getInstance().timeInMillis 202 | val caloriesBurned = ((distanceInMeters / 1000f) * weight).toInt() 203 | val run = Run( 204 | bmp, 205 | dateTimeStamp, 206 | avgSpeed, 207 | distanceInMeters, 208 | currentTimeInMillis, 209 | caloriesBurned 210 | ) 211 | viewModel.insertRun(run) 212 | Snackbar.make( 213 | requireActivity().findViewById(R.id.rootView), 214 | "Run saved successfully", 215 | Snackbar.LENGTH_LONG 216 | ).show() 217 | stopRun() 218 | } 219 | } 220 | 221 | private fun addAllPolylines() { 222 | for (polyline in pathPoints) { 223 | val polyLineOptions = PolylineOptions() 224 | .color(POLYLINE_COLOR) 225 | .width(POLYLINE_WIDTH) 226 | .addAll(polyline) 227 | map?.addPolyline(polyLineOptions) 228 | } 229 | } 230 | 231 | private fun addLatestPolyline() { 232 | if (pathPoints.isNotEmpty() && pathPoints.last().size > 1) { 233 | val preLastLatLng = pathPoints.last()[pathPoints.last().size - 2] 234 | val lastLatLng = pathPoints.last().last() 235 | val polyLineOptions = PolylineOptions() 236 | .color(POLYLINE_COLOR) 237 | .width(POLYLINE_WIDTH) 238 | .add(preLastLatLng) 239 | .add(lastLatLng) 240 | map?.addPolyline(polyLineOptions) 241 | } 242 | } 243 | 244 | private fun sendCommandToService(action: String) = 245 | Intent(requireContext(), TrackingService::class.java).also { 246 | it.action = action 247 | requireContext().startService(it) 248 | } 249 | 250 | override fun onResume() { 251 | super.onResume() 252 | mapView?.onResume() 253 | } 254 | 255 | override fun onStart() { 256 | super.onStart() 257 | mapView?.onStart() 258 | } 259 | 260 | override fun onStop() { 261 | super.onStop() 262 | mapView?.onStop() 263 | } 264 | 265 | override fun onPause() { 266 | super.onPause() 267 | mapView?.onPause() 268 | } 269 | 270 | override fun onLowMemory() { 271 | super.onLowMemory() 272 | mapView?.onLowMemory() 273 | } 274 | 275 | /*override fun onDestroy() { 276 | super.onDestroy() 277 | mapView?.onDestroy() 278 | }*/ 279 | 280 | override fun onSaveInstanceState(outState: Bundle) { 281 | super.onSaveInstanceState(outState) 282 | mapView.onSaveInstanceState(outState) 283 | } 284 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/viewmodels/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.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.animsh.runningtracker.db.Run 8 | import com.animsh.runningtracker.other.SortsType 9 | import com.animsh.runningtracker.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 | 24 | var sortType = SortsType.DATE 25 | 26 | init { 27 | runs.addSource(runsSortedByDate){result -> 28 | if(sortType == SortsType.DATE){ 29 | result?.let { runs.value = it } 30 | } 31 | } 32 | runs.addSource(runsSortedByAvgSpeed){result -> 33 | if(sortType == SortsType.AVG_SPEED){ 34 | result?.let { runs.value = it } 35 | } 36 | } 37 | runs.addSource(runsSortedByCaloriesBurned){result -> 38 | if(sortType == SortsType.CALORIES_BURNED){ 39 | result?.let { runs.value = it } 40 | } 41 | } 42 | runs.addSource(runsSortedByDistance){result -> 43 | if(sortType == SortsType.DISTANCE){ 44 | result?.let { runs.value = it } 45 | } 46 | } 47 | runs.addSource(runsSortedByTimeInMillis){result -> 48 | if(sortType == SortsType.RUNNING_TIME){ 49 | result?.let { runs.value = it } 50 | } 51 | } 52 | } 53 | 54 | fun sortRuns(sortType: SortsType) = when(sortType){ 55 | SortsType.DATE -> runsSortedByDate.value?.let { runs.value = it } 56 | SortsType.RUNNING_TIME -> runsSortedByTimeInMillis.value?.let { runs.value = it } 57 | SortsType.AVG_SPEED -> runsSortedByAvgSpeed.value?.let { runs.value = it } 58 | SortsType.DISTANCE -> runsSortedByDistance.value?.let { runs.value = it } 59 | SortsType.CALORIES_BURNED -> runsSortedByCaloriesBurned.value?.let { runs.value = it } 60 | }.also { 61 | this.sortType = sortType 62 | } 63 | 64 | fun insertRun(run: Run) = viewModelScope.launch { 65 | mainRepository.insertRun(run) 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/animsh/runningtracker/ui/viewmodels/StatisticsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.animsh.runningtracker.ui.viewmodels 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.ViewModel 5 | import com.animsh.runningtracker.repositories.MainRepository 6 | 7 | class StatisticsViewModel @ViewModelInject constructor( 8 | val mainRepository: MainRepository 9 | ) : ViewModel() { 10 | 11 | val totalTimeRun = mainRepository.getTotalTimesInMillis() 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-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /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/layout-land/fragment_statistics.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 29 | 30 | 39 | 40 | 50 | 51 | 62 | 63 | 71 | 72 | 81 | 82 | 93 | 94 | 100 | 101 | 107 | 108 | 116 | 117 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/fragment_tracking.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 30 | 31 | 41 | 42 | 53 | 54 | 55 | 56 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 21 | 22 | 36 | 37 | 38 | 39 | 40 | 48 | 49 | 56 | 57 | 58 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_run.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 27 | 28 | 37 | 38 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 26 | 27 | 28 | 29 | 40 | 41 | 48 | 49 | 50 | 51 |