Files
FitnessDroid/app/src/main/java/eu/brassepc/fitnessdroid/data/StepsSync.kt
claude 5f1d1eae4f
All checks were successful
release / build-release (push) Successful in 4m30s
Etapp 5: stegsynk via Health Connect
- StepsSync: läser dagens + gårdagens stegsumma (StepsRecord.COUNT_TOTAL)
  och upsertar till gym-API:t; synkas automatiskt (max var 10:e min) när
  hemskärmen visas
- Inställningar → Stegsynk: status, aktivera-flöde (HC-permission),
  Play-länk om Health Connect saknas, manuell synk
- Kcal-mätarens telefonrad visar nu riktiga steg + kcal (serverns
  beräkning med dubbelräkningsskydd); stegmål får riktig data
- connect-client 1.1.0-alpha07 (nyare kräver AGP 8.9/compileSdk 36 —
  toolchain-bump tas separat); HC-manifest (READ_STEPS, rationale-intents)
- Version 0.10.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg
2026-08-26 12:10:30 +02:00

81 lines
2.9 KiB
Kotlin

package eu.brassepc.fitnessdroid.data
import android.content.Context
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.time.TimeRangeFilter
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
/**
* Steg via Health Connect: läser dagens + gårdagens stegsumma (som andra
* appar/telefonen loggat) och synkar upp till gym-API:t (upsertDailySteps).
* Servern räknar sedan in stegen i kcal-mätarens telefondel och stegmålen.
*/
class StepsSync(
private val context: Context,
private val gymApi: GymApi,
) {
val stepsPermission: String = HealthPermission.getReadPermission(StepsRecord::class)
private var lastSyncMs = 0L
/** SDK_UNAVAILABLE / SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED / SDK_AVAILABLE */
fun sdkStatus(): Int = HealthConnectClient.getSdkStatus(context)
fun isAvailable(): Boolean = sdkStatus() == HealthConnectClient.SDK_AVAILABLE
suspend fun hasPermission(): Boolean {
if (!isAvailable()) return false
return runCatching {
HealthConnectClient.getOrCreate(context)
.permissionController.getGrantedPermissions()
.contains(stepsPermission)
}.getOrDefault(false)
}
/**
* Läs och synka. Returnerar dagens steg, eller null om HC saknas /
* behörighet inte getts. Fel sväljs (nästa synk försöker igen).
*/
suspend fun syncNow(): Int? {
if (!hasPermission()) return null
val client = HealthConnectClient.getOrCreate(context)
val zone = ZoneId.systemDefault()
val today = LocalDate.now()
var todaySteps: Int? = null
for (day in listOf(today.minusDays(1), today)) {
val startInstant = day.atStartOfDay(zone).toInstant()
val endInstant = if (day == today) Instant.now()
else day.plusDays(1).atStartOfDay(zone).toInstant()
val steps = runCatching {
val result = client.aggregate(
AggregateRequest(
metrics = setOf(StepsRecord.COUNT_TOTAL),
timeRangeFilter = TimeRangeFilter.between(startInstant, endInstant),
)
)
(result[StepsRecord.COUNT_TOTAL] ?: 0L).toInt()
}.getOrNull() ?: continue
if (day == today) todaySteps = steps
runCatching {
gymApi.upsertDailySteps(startInstant.toString(), steps)
}
}
lastSyncMs = System.currentTimeMillis()
return todaySteps
}
/** Synka högst var 10:e minut — anropas t.ex. när hemskärmen visas. */
suspend fun syncThrottled() {
if (System.currentTimeMillis() - lastSyncMs < 10 * 60_000) return
syncNow()
}
}