├── .idea ├── .name ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── misc.xml ├── runConfigurations.xml ├── gradle.xml └── jarRepositories.xml ├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── font │ │ │ │ ├── poppins.ttf │ │ │ │ └── poppins_bold.ttf │ │ │ ├── drawable │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── bottom_nav_selector.xml │ │ │ │ ├── ic_stop_black_24dp.xml │ │ │ │ ├── ic_add_black.xml │ │ │ │ ├── ic_delete.xml │ │ │ │ ├── ic_pause_black_24dp.xml │ │ │ │ ├── ic_graph.xml │ │ │ │ ├── view_bg.xml │ │ │ │ ├── splash_screen_background.xml │ │ │ │ ├── ic_close_white.xml │ │ │ │ ├── ic_run.xml │ │ │ │ ├── ic_directions_run_black_24dp.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ └── ic_settings.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 │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── custom_spinner.xml │ │ │ │ ├── fragment_run.xml │ │ │ │ ├── marker_view.xml │ │ │ │ ├── fragment_setup.xml │ │ │ │ ├── fragment_login.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── fragment_settings.xml │ │ │ │ ├── fragment_tracking.xml │ │ │ │ ├── item_run.xml │ │ │ │ └── fragment_statistics.xml │ │ │ ├── drawable-v23 │ │ │ │ └── splash_screen_background.xml │ │ │ ├── menu │ │ │ │ ├── toolbar_tracking_menu.xml │ │ │ │ └── bottom_nav_menu.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout-land │ │ │ │ ├── fragment_tracking.xml │ │ │ │ └── fragment_statistics.xml │ │ │ └── navigation │ │ │ │ └── nav_graph.xml │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ │ └── com │ │ │ │ └── imnstudios │ │ │ │ └── runningapp │ │ │ │ ├── other │ │ │ │ ├── SortType.kt │ │ │ │ ├── ViewUtils.kt │ │ │ │ ├── Constants.kt │ │ │ │ ├── CustomMarkerView.kt │ │ │ │ └── TrackingUtility.kt │ │ │ │ ├── BaseApplication.kt │ │ │ │ ├── db │ │ │ │ ├── RunningDatabase.kt │ │ │ │ ├── Run.kt │ │ │ │ ├── Converters.kt │ │ │ │ └── RunDao.kt │ │ │ │ ├── ui │ │ │ │ ├── SplashScreenActivity.kt │ │ │ │ ├── viewmodels │ │ │ │ │ ├── StatisticsViewModel.kt │ │ │ │ │ ├── NetworkViewModel.kt │ │ │ │ │ └── MainViewModel.kt │ │ │ │ ├── fragments │ │ │ │ │ ├── CancelTrackingDialog.kt │ │ │ │ │ ├── SetupFragment.kt │ │ │ │ │ ├── LogInFragment.kt │ │ │ │ │ ├── StatisticsFragment.kt │ │ │ │ │ ├── SettingsFragment.kt │ │ │ │ │ ├── RunFragment.kt │ │ │ │ │ └── TrackingFragment.kt │ │ │ │ └── MainActivity.kt │ │ │ │ ├── repositories │ │ │ │ └── MainRepository.kt │ │ │ │ ├── di │ │ │ │ ├── ServiceModule.kt │ │ │ │ └── AppModule.kt │ │ │ │ ├── adapters │ │ │ │ └── RunAdapter.kt │ │ │ │ └── services │ │ │ │ └── TrackingService.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── imnstudios │ │ │ └── runningapp │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── imnstudios │ │ └── runningapp │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /.idea/.name: -------------------------------------------------------------------------------- 1 | Running App -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.json -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "Running App" -------------------------------------------------------------------------------- /app/src/main/res/font/poppins.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/font/poppins.ttf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/drawable/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imnithish/running_app/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #651FFF 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 11 12:06:52 IST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_nav_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Running App 3 | 4 | Date 5 | Running Time 6 | Distance 7 | Average Speed 8 | Calories Burned 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_spinner.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/res/drawable/ic_pause_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_graph.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/view_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /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/res/drawable/splash_screen_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v23/splash_screen_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/toolbar_tracking_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /app/src/test/java/com/imnstudios/runningapp/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imnstudios.runningapp 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_white.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /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/res/menu/bottom_nav_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /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/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/res/drawable/ic_run.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_directions_run_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /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/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-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /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/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E3F2FD 4 | #BBDEFB 5 | #90CAF9 6 | #64B5F6 7 | #42A5F5 8 | #2196F3 9 | #1976D2 10 | #1565C0 11 | #0D47A1 12 | #82B1FF 13 | #448AFF 14 | #2979FF 15 | #2962FF 16 | #651FFF 17 | #D50000 18 | 19 | #FFFFFF 20 | #b2beb5 21 | #000000 22 | #651FFF 23 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /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/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/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/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/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 | } -------------------------------------------------------------------------------- /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/src/main/res/layout/fragment_run.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 28 | 29 | 38 | 39 | 52 | 53 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/res/layout/marker_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 23 | 24 | 33 | 34 | 43 | 44 | 53 | 54 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /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/res/layout/fragment_setup.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 15 | 16 | 25 | 26 | 27 | 45 | 46 | 54 | 55 | 56 | 57 | 58 | 59 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/fragment_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 16 | 17 | 35 | 36 | 44 | 45 |