Skip to content

Commit

Permalink
Health Connect migration
Browse files Browse the repository at this point in the history
  • Loading branch information
feelsoftware committed Jan 8, 2025
1 parent dbee303 commit 61c9b43
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 29 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## FeelFine - activity tracker app

Application to track your fitness activities with [Google Fit API](https://developers.google.com/fit "Google Fit API").
Application to track your fitness activities with [Health Connect](https://developer.android.com/health-and-fitness/guides/health-connect "Health Connect") and [Google Fit](https://developers.google.com/fit "Google Fit") APIs.
- Kotlin
- Jetpack Compose
- [Koin](https://github.com/InsertKoinIO/koin "Koin") for DI
Expand Down Expand Up @@ -36,7 +36,7 @@ Four simple onboarding steps to pick your name, gender, weight and birthday.

<img src="https://github.com/feelsoftware/FeelFine/raw/main/readme/score_1.png" width="180" height="360" /> <img src="https://github.com/feelsoftware/FeelFine/raw/main/readme/score_2.png" width="180" height="360" /> <img src="https://github.com/feelsoftware/FeelFine/raw/main/readme/score_3.png" width="180" height="360" /> <img src="https://github.com/feelsoftware/FeelFine/raw/main/readme/score_4.png" width="180" height="360" />

A user’s data as steps, sleep, biking, running and other activities synced via [Google Fit API](https://developers.google.com/fit "Google Fit API"). User has an every day score, based on his own metrics.
Users data as steps, sleep, walking, running and other combined activities are synced via [Health Connect](https://developer.android.com/health-and-fitness/guides/health-connect "Health Connect") or [Google Fit](https://developers.google.com/fit "Google Fit") APIs. Users have an every day score, based on their own metrics.


------------
Expand All @@ -46,7 +46,7 @@ A user’s data as steps, sleep, biking, running and other activities synced via

<img src="https://github.com/feelsoftware/FeelFine/raw/main/readme/statistic_1.png" width="180" height="360" /> <img src="https://github.com/feelsoftware/FeelFine/raw/main/readme/statistic_2.png" width="180" height="360" /> <img src="https://github.com/feelsoftware/FeelFine/raw/main/readme/statistic_3.png" width="180" height="360" />

Users could observe week/month/custom range activities statistics.
Users could observe week/month/custom range activities statistics.


------------
Expand All @@ -56,7 +56,7 @@ Users could observe week/month/custom range activities statistics.

<img src="https://github.com/feelsoftware/FeelFine/raw/main/readme/mood.png" width="180" height="360" />

We are asking each day 'How are you?' for the mood score with 9 options for users.
Every day we are asking users 'How are you?' for the mood score with 9 options.


------------
Expand All @@ -68,3 +68,13 @@ We are asking each day 'How are you?' for the mood score with 9 options for user

User profile with wage, age and goals (steps, sleep, activity) customizations.


------------


### What's next
- Migrate to Kotlin Coroutines from RxJava
- Migrate to Compose
- Migrate to Kotlin Multiplatform (use cross-platform library https://github.com/vitoksmile/HealthKMP)
- Night theme
- Edit goals
4 changes: 4 additions & 0 deletions app/src/main/java/com/feelsoftware/feelfine/di/KoinInit.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.feelsoftware.feelfine.di

import android.app.Application
import com.feelsoftware.feelfine.fit.FitPermissionManager
import com.feelsoftware.feelfine.ui.onboarding.onboardingModule
import com.feelsoftware.feelfine.utils.ActivityEngine
import org.koin.android.ext.koin.androidContext
Expand All @@ -21,7 +22,10 @@ object KoinInit {
utilsModule,
onboardingModule,
)
// FIXME: ActivityEngine to initialize ActivityLifecycleCallbacks
koin.get<ActivityEngine>()
// FIXME: FitPermissionManager to initialize HealthConnectFitPermissionManagerWrapper#activityEngine
koin.get<FitPermissionManager>()
}
}
}
93 changes: 74 additions & 19 deletions app/src/main/java/com/feelsoftware/feelfine/di/fit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,112 @@ package com.feelsoftware.feelfine.di
import com.feelsoftware.feelfine.data.db.dao.ActivityDao
import com.feelsoftware.feelfine.data.db.dao.SleepDao
import com.feelsoftware.feelfine.data.db.dao.StepsDao
import com.feelsoftware.feelfine.data.repository.*
import com.feelsoftware.feelfine.data.repository.ActivityDataRepository
import com.feelsoftware.feelfine.data.repository.ActivityRemoteDataSource
import com.feelsoftware.feelfine.data.repository.SleepDataRepository
import com.feelsoftware.feelfine.data.repository.SleepRemoteDataSource
import com.feelsoftware.feelfine.data.repository.StepsDataRepository
import com.feelsoftware.feelfine.data.repository.StepsRemoteDataSource
import com.feelsoftware.feelfine.data.repository.UserRepository
import com.feelsoftware.feelfine.fit.FitPermissionManager
import com.feelsoftware.feelfine.fit.FitRepository
import com.feelsoftware.feelfine.fit.GoogleFitPermissionManager
import com.feelsoftware.feelfine.fit.GoogleFitRepository
import com.feelsoftware.feelfine.fit.HealthConnectClientProvider
import com.feelsoftware.feelfine.fit.HealthConnectClientProviderImpl
import com.feelsoftware.feelfine.fit.HealthConnectFitPermissionManagerWrapper
import com.feelsoftware.feelfine.fit.HealthConnectFitRepositoryWrapper
import com.feelsoftware.feelfine.fit.HealthConnectPermissionManager
import com.feelsoftware.feelfine.fit.HealthConnectPermissionManagerImpl
import com.feelsoftware.feelfine.fit.HealthConnectRepository
import com.feelsoftware.feelfine.fit.HealthConnectRepositoryImpl
import com.feelsoftware.feelfine.fit.mock.MockFitRepository
import com.feelsoftware.feelfine.fit.usecase.GetFitDataUseCase
import com.feelsoftware.feelfine.utils.ActivityEngine
import org.koin.android.ext.koin.androidApplication
import org.koin.core.scope.Scope
import org.koin.dsl.module

val fitModule = module {
single<HealthConnectClientProvider> {
HealthConnectClientProviderImpl(
context = androidApplication(),
)
}
single<HealthConnectPermissionManager> {
HealthConnectPermissionManagerImpl(
clientProvider = get<HealthConnectClientProvider>(),
)
}
factory<HealthConnectRepository> {
HealthConnectRepositoryImpl(
clientProvider = get<HealthConnectClientProvider>(),
permissionManager = get<HealthConnectPermissionManager>(),
)
}
factory<FitRepository> {
val profile = get<UserRepository>().getProfileLegacy().firstOrError().blockingGet()
if (profile.isDemo) {
MockFitRepository()
} else {
GoogleFitRepository(get<ActivityEngine>(), get<FitPermissionManager>())
if (hasHealthConnect) {
HealthConnectFitRepositoryWrapper(
repository = get<HealthConnectRepository>(),
)
} else {
GoogleFitRepository(
activityEngine = get<ActivityEngine>(),
permissionManager = get<FitPermissionManager>(),
)
}
}
}
single<FitPermissionManager> {
GoogleFitPermissionManager(
get<ActivityDao>(),
get<ActivityEngine>(),
get<SleepDao>(),
get<StepsDao>(),
get<UserRepository>(),
)
if (hasHealthConnect) {
HealthConnectFitPermissionManagerWrapper(
activityDao = get<ActivityDao>(),
activityEngine = get<ActivityEngine>(),
permissionManager = get<HealthConnectPermissionManager>(),
sleepDao = get<SleepDao>(),
stepsDao = get<StepsDao>(),
userRepository = get<UserRepository>(),
)
} else {
GoogleFitPermissionManager(
activityDao = get<ActivityDao>(),
activityEngine = get<ActivityEngine>(),
sleepDao = get<SleepDao>(),
stepsDao = get<StepsDao>(),
userRepository = get<UserRepository>(),
)
}
}
factory<GetFitDataUseCase> {
GetFitDataUseCase(
get<StepsDataRepository>(),
get<SleepDataRepository>(),
get<ActivityDataRepository>(),
stepsRepository = get<StepsDataRepository>(),
sleepRepository = get<SleepDataRepository>(),
activityRepository = get<ActivityDataRepository>(),
)
}
factory<StepsDataRepository> {
StepsDataRepository(
get<StepsDao>(),
StepsRemoteDataSource(get<FitRepository>())
localDataSource = get<StepsDao>(),
remoteDataSource = StepsRemoteDataSource(get<FitRepository>())
)
}
factory<SleepDataRepository> {
SleepDataRepository(
get<SleepDao>(),
SleepRemoteDataSource(get<FitRepository>())
localDataSource = get<SleepDao>(),
remoteDataSource = SleepRemoteDataSource(get<FitRepository>())
)
}
factory<ActivityDataRepository> {
ActivityDataRepository(
get<ActivityDao>(),
ActivityRemoteDataSource(get<FitRepository>())
localDataSource = get<ActivityDao>(),
remoteDataSource = ActivityRemoteDataSource(get<FitRepository>())
)
}
}
}

private inline val Scope.hasHealthConnect: Boolean
get() = get<HealthConnectClientProvider>().invoke().getOrNull() != null
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface FitPermissionManager {

private const val REQUEST_CODE = 1717

@Deprecated("Migrate to HealthConnectPermissionManager")
class GoogleFitPermissionManager(
private val activityDao: ActivityDao,
private val activityEngine: ActivityEngine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface FitRepository {
fun getActivity(startTime: Date, endTime: Date): Single<List<ActivityInfo>>
}

@Deprecated("Migrate to HealthConnectRepository")
class GoogleFitRepository(
private val activityEngine: ActivityEngine,
private val permissionManager: FitPermissionManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.time.TimeRangeFilter
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.StateFlow
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
Expand Down Expand Up @@ -51,9 +51,11 @@ interface HealthConnectRepository {

class HealthConnectRepositoryImpl(
private val clientProvider: HealthConnectClientProvider,
private val permissionManager: HealthConnectPermissionManager,
permissionManager: HealthConnectPermissionManager,
) : HealthConnectRepository {

private val hasPermission: StateFlow<Boolean> = permissionManager.hasPermission()

override suspend fun getActivity(date: LocalDate): Result<Activity> {
return get<ExerciseSessionRecord>(
startTime = date.dayStart(),
Expand Down Expand Up @@ -114,7 +116,7 @@ class HealthConnectRepositoryImpl(
startTime: LocalDateTime,
endTime: LocalDateTime,
): Result<List<T>> = runCatching {
if (!permissionManager.hasPermission().first()) {
if (!hasPermission.value) {
return@runCatching emptyList<T>()
}
val client = clientProvider().getOrThrow()
Expand All @@ -137,7 +139,7 @@ class HealthConnectRepositoryImpl(
endTime: LocalDateTime,
metrics: Set<AggregateMetric<Number>>,
): Result<Map<AggregateMetric<Number>, Number>> = runCatching {
if (!permissionManager.hasPermission().first()) {
if (!hasPermission.value) {
return@runCatching emptyMap()
}
val client = clientProvider().getOrThrow()
Expand Down
Loading

0 comments on commit 61c9b43

Please sign in to comment.