From a7f6d5e2fe5ea4d77f6d5955b20a6078668f0226 Mon Sep 17 00:00:00 2001 From: Ramzi Jabali Date: Tue, 4 Jun 2024 15:17:38 -0700 Subject: [PATCH] Added retrofit dependencies, created quotable API DI, created retrofit DI, Created Statistics viewmodel, finished Statistics view --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 1 + .../ramzi/eljabali/justjog/MainActivity.kt | 7 +- .../ramzi/eljabali/justjog/koin/Module.kt | 23 ++- .../motivationalquotes/MotivationQuotesAPI.kt | 9 + .../motivationalquotes/MotivationalQuote.kt | 22 +++ .../eljabali/justjog/usecase/JogUseCase.kt | 24 ++- .../eljabali/justjog/util/DurationFormat.kt | 4 +- .../justjog/util/JustJogApplication.kt | 5 +- .../java/ramzi/eljabali/justjog/util/Time.kt | 16 +- .../justjog/viewmodel/JogViewModel.kt | 7 - .../justjog/viewmodel/StatisticsViewModel.kt | 156 ++++++++++++++++++ .../justjog/viewstate/StatisticsViewState.kt | 10 ++ .../workmanager/JogSummaryWorkManager.kt | 2 +- gradle/libs.versions.toml | 3 + 15 files changed, 270 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/ramzi/eljabali/justjog/motivationalquotes/MotivationQuotesAPI.kt create mode 100644 app/src/main/java/ramzi/eljabali/justjog/motivationalquotes/MotivationalQuote.kt delete mode 100644 app/src/main/java/ramzi/eljabali/justjog/viewmodel/JogViewModel.kt create mode 100644 app/src/main/java/ramzi/eljabali/justjog/viewmodel/StatisticsViewModel.kt create mode 100644 app/src/main/java/ramzi/eljabali/justjog/viewstate/StatisticsViewState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6789d92..890a06c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -79,6 +79,8 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.work.manager) implementation(libs.koin.di) + implementation(libs.com.squareup.retrofit2) + implementation(libs.com.squareup.retrofit2.converter) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b71cd8..3fd5a2a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + () + setContent { val navController = rememberNavController() JustJogTheme(true) { diff --git a/app/src/main/java/ramzi/eljabali/justjog/koin/Module.kt b/app/src/main/java/ramzi/eljabali/justjog/koin/Module.kt index 9da9c10..eb60bec 100644 --- a/app/src/main/java/ramzi/eljabali/justjog/koin/Module.kt +++ b/app/src/main/java/ramzi/eljabali/justjog/koin/Module.kt @@ -4,15 +4,18 @@ import androidx.room.Room import org.koin.android.ext.koin.androidApplication import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module +import ramzi.eljabali.justjog.motivationalquotes.MotivationQuotesAPI import ramzi.eljabali.justjog.repository.room.database.JustJogDataBase import ramzi.eljabali.justjog.repository.room.jogentries.JogEntryDAO import ramzi.eljabali.justjog.repository.room.jogsummary.JogSummaryDAO import ramzi.eljabali.justjog.repository.room.jogsummarytemp.JogSummaryTempDAO import ramzi.eljabali.justjog.usecase.JogUseCase -import ramzi.eljabali.justjog.viewmodel.JogViewModel +import ramzi.eljabali.justjog.viewmodel.StatisticsViewModel +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory val statisticsModule = module { - viewModel { JogViewModel() } + viewModel { StatisticsViewModel(get(), get()) } } val jogDataBaseModule = module { @@ -28,5 +31,19 @@ val jogDataBaseModule = module { } val jogUseCaseModule = module { - single { JogUseCase(get(), get(), get()) } + single { JogUseCase(get(), get(), get()) } +} + +const val BASE_URL = "https://api.quotable.io/" +val networkModule = module { + single { get().create(MotivationQuotesAPI::class.java) } + single { + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } +} +val viewModelModule = module { + viewModel { StatisticsViewModel(get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/ramzi/eljabali/justjog/motivationalquotes/MotivationQuotesAPI.kt b/app/src/main/java/ramzi/eljabali/justjog/motivationalquotes/MotivationQuotesAPI.kt new file mode 100644 index 0000000..a9847d6 --- /dev/null +++ b/app/src/main/java/ramzi/eljabali/justjog/motivationalquotes/MotivationQuotesAPI.kt @@ -0,0 +1,9 @@ +package ramzi.eljabali.justjog.motivationalquotes + +import retrofit2.Response +import retrofit2.http.GET + +interface MotivationQuotesAPI { + @GET("/quotes/random?minLength=100&maxLength=140&tags=Motivational/") + suspend fun getQuote(): Response> +} \ No newline at end of file diff --git a/app/src/main/java/ramzi/eljabali/justjog/motivationalquotes/MotivationalQuote.kt b/app/src/main/java/ramzi/eljabali/justjog/motivationalquotes/MotivationalQuote.kt new file mode 100644 index 0000000..a4b0739 --- /dev/null +++ b/app/src/main/java/ramzi/eljabali/justjog/motivationalquotes/MotivationalQuote.kt @@ -0,0 +1,22 @@ +package ramzi.eljabali.justjog.motivationalquotes + +import com.google.gson.annotations.SerializedName + +data class MotivationalQuote( + @SerializedName("_id") + val id: String, + @SerializedName("content") + val quote: String, + @SerializedName("author") + val author: String, + @SerializedName("tags") + val tags: Array, + @SerializedName("authorSlug") + val authorSlug: String, + @SerializedName("length") + val length: Int, + @SerializedName("dateAdded") + val dateAdded: String, + @SerializedName("dateModified") + val dateModified: String +) diff --git a/app/src/main/java/ramzi/eljabali/justjog/usecase/JogUseCase.kt b/app/src/main/java/ramzi/eljabali/justjog/usecase/JogUseCase.kt index 351583e..25ce557 100644 --- a/app/src/main/java/ramzi/eljabali/justjog/usecase/JogUseCase.kt +++ b/app/src/main/java/ramzi/eljabali/justjog/usecase/JogUseCase.kt @@ -4,10 +4,12 @@ import android.util.Log import com.google.android.gms.maps.model.LatLng import javatimefun.zoneddatetime.extensions.print import javatimefun.zoneddatetime.extensions.toZonedDateTime +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.single +import ramzi.eljabali.justjog.motivationalquotes.MotivationQuotesAPI import ramzi.eljabali.justjog.repository.room.jogentries.JogEntry import ramzi.eljabali.justjog.repository.room.jogentries.JogEntryDAO import ramzi.eljabali.justjog.repository.room.jogsummary.JogSummary @@ -17,6 +19,7 @@ import ramzi.eljabali.justjog.repository.room.jogsummarytemp.JogSummaryTempDAO import ramzi.eljabali.justjog.util.DateFormat import ramzi.eljabali.justjog.util.TAG import java.time.Duration +import java.time.ZonedDateTime class JogUseCase( private val jogEntryDao: JogEntryDAO, @@ -36,13 +39,13 @@ class JogUseCase( // SUMMARY suspend fun getNewJogID(): Int { Log.i(TAG, "getNewJogID() -> JogUseCase") - val id = jogSummaryDao.getLastID().firstOrNull() + val id = jogSummaryDao.getLastID().firstOrNull() return if (id == null) { Log.i(TAG, "getNewJogID() -> 1") 1 } else { - Log.i(TAG, "getNewJogID() -> ${id+1}") + Log.i(TAG, "getNewJogID() -> ${id + 1}") id + 1 } } @@ -58,6 +61,22 @@ class JogUseCase( ) } + fun getJogSummariesBetweenDates( + startDate: ZonedDateTime, + endDate: ZonedDateTime + ): Flow> = + jogSummaryDao.getBetweenDatesUseCase(startDate, endDate).map { list -> + list.map { + ModifiedJogSummary( + jogId = it.id, + startDate = it.startDate.toZonedDateTime(DateFormat.YYYY_MM_DD_T_TIME.format)!!, + duration = Duration.ofSeconds(it.totalJogDuration), + totalDistance = it.totalJogDistance + ) + } + } + + // SUMMARY TEMP fun getJogSummaryTemp(id: Int) = jogSummaryTempDao.getById(id).map { jogSummaryTemp -> if (jogSummaryTemp != null) { @@ -72,6 +91,7 @@ class JogUseCase( null } } + fun deleteAllTempJogSummaries() = jogSummaryTempDao.deleteAll() fun addOrUpdateJogSummaryTemp(modifiedTempJogSummary: ModifiedTempJogSummary) { jogSummaryTempDao.add( diff --git a/app/src/main/java/ramzi/eljabali/justjog/util/DurationFormat.kt b/app/src/main/java/ramzi/eljabali/justjog/util/DurationFormat.kt index 3f2c193..9cd80df 100644 --- a/app/src/main/java/ramzi/eljabali/justjog/util/DurationFormat.kt +++ b/app/src/main/java/ramzi/eljabali/justjog/util/DurationFormat.kt @@ -2,5 +2,7 @@ package ramzi.eljabali.justjog.util enum class DurationFormat(val format: String) { HH_MM_SS("hh:mm:ss"), - H_M_S("h:m:s") + H_M_S("h:m:s"), + HMS("h m s"), + MS("m s") } \ No newline at end of file diff --git a/app/src/main/java/ramzi/eljabali/justjog/util/JustJogApplication.kt b/app/src/main/java/ramzi/eljabali/justjog/util/JustJogApplication.kt index f553deb..abbce59 100644 --- a/app/src/main/java/ramzi/eljabali/justjog/util/JustJogApplication.kt +++ b/app/src/main/java/ramzi/eljabali/justjog/util/JustJogApplication.kt @@ -13,11 +13,14 @@ import org.koin.core.context.GlobalContext.startKoin import ramzi.eljabali.justjog.R import ramzi.eljabali.justjog.koin.jogDataBaseModule import ramzi.eljabali.justjog.koin.jogUseCaseModule +import ramzi.eljabali.justjog.koin.networkModule import ramzi.eljabali.justjog.koin.statisticsModule class JustJogApplication : Application() { private val CHANNEL_ID_1 = "JUST_JOG_1" - private val modules = listOf(statisticsModule, jogDataBaseModule, jogUseCaseModule) + private val modules = + listOf(statisticsModule, jogDataBaseModule, jogUseCaseModule, networkModule) + override fun onCreate() { super.onCreate() startKoin { diff --git a/app/src/main/java/ramzi/eljabali/justjog/util/Time.kt b/app/src/main/java/ramzi/eljabali/justjog/util/Time.kt index 22833b4..44b8169 100644 --- a/app/src/main/java/ramzi/eljabali/justjog/util/Time.kt +++ b/app/src/main/java/ramzi/eljabali/justjog/util/Time.kt @@ -30,12 +30,18 @@ fun getFormattedTime(totalTimeInSeconds: Long, format: DurationFormat): String { } -fun getFormattedTimeMinutes(totalTimeInMinutes: Long): String { - var tempTime = minutesToSeconds(totalTimeInMinutes) +fun getFormattedTimeSeconds(totalTimeInSeconds: Long, format: DurationFormat = DurationFormat.HH_MM_SS): String { + var tempTime = totalTimeInSeconds val totalHours = secondsToHours(tempTime) - tempTime -= hoursToSeconds(totalHours) + tempTime %= 3600 val totalMinutes = secondsToMinutes(tempTime) - tempTime -= minutesToSeconds(totalMinutes) + tempTime %= 60 val totalSeconds = tempTime - return String.format("%02d:%02d:%02d", totalHours, totalMinutes, totalSeconds) + + return when (format) { + DurationFormat.HH_MM_SS -> String.format("%02d:%02d:%02d", totalHours, totalMinutes, totalSeconds) + DurationFormat.H_M_S -> String.format("%d:%d:%d", totalHours, totalMinutes, totalSeconds) + DurationFormat.HMS -> String.format("%02dh %02dm %02ds", totalHours, totalMinutes, totalSeconds) + DurationFormat.MS -> String.format("%01dm %02ds", totalHours, totalMinutes, totalSeconds) + } } \ No newline at end of file diff --git a/app/src/main/java/ramzi/eljabali/justjog/viewmodel/JogViewModel.kt b/app/src/main/java/ramzi/eljabali/justjog/viewmodel/JogViewModel.kt deleted file mode 100644 index 7839554..0000000 --- a/app/src/main/java/ramzi/eljabali/justjog/viewmodel/JogViewModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ramzi.eljabali.justjog.viewmodel - -import androidx.lifecycle.ViewModel - - -class JogViewModel : ViewModel() { -} \ No newline at end of file diff --git a/app/src/main/java/ramzi/eljabali/justjog/viewmodel/StatisticsViewModel.kt b/app/src/main/java/ramzi/eljabali/justjog/viewmodel/StatisticsViewModel.kt new file mode 100644 index 0000000..8fe3b15 --- /dev/null +++ b/app/src/main/java/ramzi/eljabali/justjog/viewmodel/StatisticsViewModel.kt @@ -0,0 +1,156 @@ +package ramzi.eljabali.justjog.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.eljabali.joggingapplicationandroid.util.getFormattedTimeSeconds +import com.jaikeerthick.composable_graphs.composables.line.model.LineData +import javatimefun.zoneddatetime.ZonedDateTimes +import javatimefun.zoneddatetime.extensions.compareDay +import javatimefun.zoneddatetime.extensions.getLast +import javatimefun.zoneddatetime.extensions.getNext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.datetime.DayOfWeek +import ramzi.eljabali.justjog.motivationalquotes.MotivationQuotesAPI +import ramzi.eljabali.justjog.usecase.JogUseCase +import ramzi.eljabali.justjog.usecase.ModifiedJogSummary +import ramzi.eljabali.justjog.util.DurationFormat +import ramzi.eljabali.justjog.util.TAG +import ramzi.eljabali.justjog.viewstate.StatisticsViewState +import java.time.Duration +import java.time.ZonedDateTime +import kotlin.math.min + + +class StatisticsViewModel( + private val jogUseCase: JogUseCase, + private val motivationQuotesAPI: MotivationQuotesAPI +) : ViewModel() { + private val _statisticsViewState = MutableStateFlow(StatisticsViewState()) + val statisticsViewState: StateFlow = _statisticsViewState.asStateFlow() + + companion object { + private val daysOfTheWeek = listOf("Mon", "Tues", "Wed", "Thurs", "Fri", "Sat", "Sun") + } + + fun onLaunch() { + viewModelScope.launch { + val quote = getQuote() + val startOfWeekDate = ZonedDateTimes.today.getLast( + DayOfWeek.MONDAY, + countingInThisDay = true + ) + val endOfWeekDate = + ZonedDateTimes.today.getNext(DayOfWeek.SUNDAY, countingInThisDay = true) + val thisWeeksJogSummaries = getWeeklyJogSummaries(startOfWeekDate, endOfWeekDate) + val chartStatistics = + getChartData(thisWeeksJogSummaries, startOfWeekDate, endOfWeekDate) + val weeklyStatisticsBreakDown = + getWeeklyStatisticsBreakDown(thisWeeksJogSummaries) + val jogStatisticsBreakDown = getWeeklyPerJogStatisticsBreakDown(thisWeeksJogSummaries) + _statisticsViewState.update { + it.copy( + quote = quote, + lineDataList = chartStatistics, + weeklyStatisticsBreakDown = weeklyStatisticsBreakDown, + perJogStatisticsBreakDown =jogStatisticsBreakDown + ) + } + } + } + + private fun getChartData( + summaryList: List, + startOfWeek: ZonedDateTime, + endOfWeek: ZonedDateTime + ): List { + val listOfLineData = mutableListOf() + var tempDate = startOfWeek + var dayOfWeek = 0 + var totalMilesInDay = 0.0 + for (summary in summaryList) { + if (tempDate.compareDay(summary.startDate) != 0) { + listOfLineData.add(LineData(x = daysOfTheWeek[dayOfWeek], y = totalMilesInDay)) + totalMilesInDay = 0.0 + tempDate = tempDate.plusDays(1) + dayOfWeek++ + } + totalMilesInDay += summary.totalDistance + } + if (tempDate.compareDay(endOfWeek) != 0) { + repeat(6 - dayOfWeek) { + listOfLineData.add(LineData(x = daysOfTheWeek[dayOfWeek], y = 0.0)) + dayOfWeek++ + } + } + return listOfLineData + } + + private fun getWeeklyStatisticsBreakDown( + weeklyJogSummaries: List + ): List { + val totalJogs = "${weeklyJogSummaries.size} Runs" + var totalMiles = 0.0 + var totalDuration = Duration.ZERO + for (summary in weeklyJogSummaries) { + totalMiles += summary.totalDistance + totalDuration = totalDuration.plus(summary.duration) + } + return listOf( + totalJogs, + "$totalMiles Miles", + getFormattedTimeSeconds(totalDuration.seconds, DurationFormat.HMS) + ) + } + + private fun getWeeklyPerJogStatisticsBreakDown(thisWeeksJogSummaries: List): List { + val averageMilesPerJog = + thisWeeksJogSummaries.sumOf { it.totalDistance } / thisWeeksJogSummaries.size + val averageDurationPerJog = + thisWeeksJogSummaries.sumOf { it.duration.seconds } / thisWeeksJogSummaries.size + val minutesPerMile = averageDurationPerJog / averageMilesPerJog + return listOf( + "$averageMilesPerJog Miles", + "$minutesPerMile Mins/Mil", + getFormattedTimeSeconds(averageDurationPerJog, DurationFormat.MS) + ) + } + + private suspend fun getWeeklyJogSummaries( + startOfWeek: ZonedDateTime, + endOfWeek: ZonedDateTime + ): List { + val result = viewModelScope.async { + jogUseCase.getJogSummariesBetweenDates( + startOfWeek, endOfWeek + ).first() + }.await() + return result + } + + + private suspend fun getQuote(): String { + val quote = viewModelScope.async(Dispatchers.IO) { + var body = "" + try { + val result = motivationQuotesAPI.getQuote().body() + body = if (result != null) { + result[0].quote + } else { + "" + } + } catch (e: Exception) { + Log.e(this.TAG, "${e.message}") + } + body + } + return quote.await() + } +} \ No newline at end of file diff --git a/app/src/main/java/ramzi/eljabali/justjog/viewstate/StatisticsViewState.kt b/app/src/main/java/ramzi/eljabali/justjog/viewstate/StatisticsViewState.kt new file mode 100644 index 0000000..160c3c2 --- /dev/null +++ b/app/src/main/java/ramzi/eljabali/justjog/viewstate/StatisticsViewState.kt @@ -0,0 +1,10 @@ +package ramzi.eljabali.justjog.viewstate + +import com.jaikeerthick.composable_graphs.composables.line.model.LineData + +data class StatisticsViewState( + val quote: String = "", + val lineDataList: List = emptyList(), + val weeklyStatisticsBreakDown: List = emptyList(), + val perJogStatisticsBreakDown: List = emptyList() +) \ No newline at end of file diff --git a/app/src/main/java/ramzi/eljabali/justjog/workmanager/JogSummaryWorkManager.kt b/app/src/main/java/ramzi/eljabali/justjog/workmanager/JogSummaryWorkManager.kt index 8eb577f..5e8c530 100644 --- a/app/src/main/java/ramzi/eljabali/justjog/workmanager/JogSummaryWorkManager.kt +++ b/app/src/main/java/ramzi/eljabali/justjog/workmanager/JogSummaryWorkManager.kt @@ -38,7 +38,7 @@ class JogSummaryWorkManager( val currentDateTime: ZonedDateTime = inputData.getString("DATE_TIME") ?.toZonedDateTime(DateFormat.YYYY_MM_DD_T_TIME.format)!! - var jogSummaryTemp: ModifiedTempJogSummary? = null + var jogSummaryTemp: ModifiedTempJogSummary? withContext(Dispatchers.IO) async@{ try { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec23b35..d91af02 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ foundation = "1.4.3" workManager = "2.9.0" koinDI = "3.6.0-wasm-alpha2" playServicesMaps = "18.2.0" +retrofit = "2.9.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.13.0-alpha05" } @@ -54,6 +55,8 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa work-manager = {group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager"} koin-di = {group = "io.insert-koin", name = "koin-android", version.ref = "koinDI"} play-services-maps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesMaps" } +com-squareup-retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +com-squareup-retrofit2-converter = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }