Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
434 changes: 155 additions & 279 deletions app/src/main/java/com/jdluu/flexinsight/data/ai/HevyAiDataAccessor.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@ private val Context.legacyApiKeyDataStore: DataStore<Preferences> by preferences
name = "api_key_preferences"
)

/**
* Read-only view of Hevy API key availability, for callers that only need to
* check connectivity status (e.g. AI Trainer pre-chat sync).
*/
interface ApiKeyStatusSource {
suspend fun hasApiKey(): Boolean
}

/**
* Stores the Hevy API key in encrypted shared preferences.
* Migrates keys from the legacy DataStore on first access.
*/
@Singleton
class ApiKeyManager @Inject constructor(
private val context: Context
) {
) : ApiKeyStatusSource {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
Expand Down Expand Up @@ -75,8 +83,7 @@ class ApiKeyManager @Inject constructor(
_apiKeyFlow.value = null
}

suspend fun hasApiKey(): Boolean = !getApiKey().isNullOrBlank()

override suspend fun hasApiKey(): Boolean = !getApiKey().isNullOrBlank()
fun isValidApiKeyFormat(apiKey: String): Boolean {
return apiKey.isNotBlank() && apiKey.length >= 10
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import kotlinx.coroutines.flow.first
import javax.inject.Inject
import javax.inject.Singleton

/** Notified when a full Hevy sync finishes, to run post-sync follow-ups. */
interface SyncCompleteListener {
suspend fun onSyncComplete()
}

@Singleton
class SyncCoordinator @Inject constructor(
private val workoutRepository: WorkoutRepository,
private val syncPreferencesManager: SyncPreferencesManager,
private val healthConnectRepository: HealthConnectRepository
) {
suspend fun onSyncComplete() {
) : SyncCompleteListener {
override suspend fun onSyncComplete() {
val count = workoutRepository.getWorkoutCount().first()
syncPreferencesManager.recordSyncSuccess(count)
val recent = workoutRepository.getRecentWorkouts(limit = 5).first()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

/** Trigger for user-initiated immediate syncs (e.g. pull to refresh, AI Trainer pre-chat). */
interface ManualSyncScheduler {
fun syncNow()
}

/**
* Manages background synchronization tasks using WorkManager.
* Centralizes sync scheduling for the entire application.
*/
@Singleton
class SyncManager @Inject constructor(
@param:ApplicationContext private val context: Context
) {
) : ManualSyncScheduler {
private val workManager = WorkManager.getInstance(context)

companion object {
Expand All @@ -28,7 +33,7 @@ class SyncManager @Inject constructor(
* Enqueues an immediate one-time sync task.
* This is useful when the user completes a workout or manually pulls to refresh.
*/
fun syncNow() {
override fun syncNow() {
AppLogger.d("Requesting immediate background sync", tag = TAG)

val constraints = Constraints.Builder()
Expand Down
34 changes: 34 additions & 0 deletions app/src/main/java/com/jdluu/flexinsight/di/SyncModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.jdluu.flexinsight.di

import com.jdluu.flexinsight.data.preferences.ApiKeyManager
import com.jdluu.flexinsight.data.preferences.ApiKeyStatusSource
import com.jdluu.flexinsight.data.sync.ManualSyncScheduler
import com.jdluu.flexinsight.data.sync.SyncCompleteListener
import com.jdluu.flexinsight.data.sync.SyncCoordinator
import com.jdluu.flexinsight.data.sync.SyncManager
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

/**
* Binds narrow sync/api-key abstractions used by view models so they can be
* faked in unit tests without touching WorkManager or encrypted storage.
*/
@Module
@InstallIn(SingletonComponent::class)
abstract class SyncModule {

@Binds
@Singleton
abstract fun bindApiKeyStatusSource(apiKeyManager: ApiKeyManager): ApiKeyStatusSource

@Binds
@Singleton
abstract fun bindManualSyncScheduler(syncManager: SyncManager): ManualSyncScheduler

@Binds
@Singleton
abstract fun bindSyncCompleteListener(syncCoordinator: SyncCoordinator): SyncCompleteListener
}
Loading
Loading