diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 81f8bf1..3a49b36 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
diff --git a/app/src/main/java/com/uc/caffeine/data/HealthConnectManager.kt b/app/src/main/java/com/uc/caffeine/data/HealthConnectManager.kt
index 71c3406..54fe6ef 100644
--- a/app/src/main/java/com/uc/caffeine/data/HealthConnectManager.kt
+++ b/app/src/main/java/com/uc/caffeine/data/HealthConnectManager.kt
@@ -3,12 +3,15 @@ package com.uc.caffeine.data
import android.content.Context
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.records.HydrationRecord
import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.SleepSessionRecord
import androidx.health.connect.client.records.metadata.Metadata as HCMetadata
import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.time.TimeRangeFilter
import androidx.health.connect.client.units.Mass
+import androidx.health.connect.client.units.Volume
import com.uc.caffeine.data.model.ConsumptionEntry
import java.time.Duration
import java.time.Instant
@@ -36,8 +39,9 @@ class HealthConnectManager(private val context: Context) {
private val nutritionWrite = HealthPermission.getWritePermission(NutritionRecord::class)
private val nutritionRead = HealthPermission.getReadPermission(NutritionRecord::class)
private val sleepRead = HealthPermission.getReadPermission(SleepSessionRecord::class)
+ private val hydrationWrite = HealthPermission.getWritePermission(HydrationRecord::class)
- val allPermissions = setOf(nutritionWrite, nutritionRead, sleepRead)
+ val allPermissions = setOf(nutritionWrite, nutritionRead, sleepRead, hydrationWrite)
val sleepPermissions = setOf(sleepRead)
fun isAvailable(): Boolean =
@@ -62,11 +66,15 @@ class HealthConnectManager(private val context: Context) {
suspend fun hasSleepPermission(): Boolean =
grantedPermissionsOrNull()?.contains(sleepRead) == true
+ suspend fun hasHydrationWritePermission(): Boolean =
+ grantedPermissionsOrNull()?.contains(hydrationWrite) == true
+
/** True only when Health Connect answered and the sleep permission is definitively not granted. */
suspend fun isSleepPermissionRevoked(): Boolean =
grantedPermissionsOrNull()?.let { sleepRead !in it } == true
private fun clientRecordId(entryId: Int): String = "caffeine_entry_$entryId"
+ private fun clientHydrationRecordId(entryId: Int): String = "caffeine_entry_${entryId}_hydration"
private fun ConsumptionEntry.toNutritionRecord(zoneId: ZoneId): NutritionRecord {
val start = Instant.ofEpochMilli(startedAtMillis)
@@ -86,14 +94,37 @@ class HealthConnectManager(private val context: Context) {
)
}
- suspend fun writeEntry(entry: ConsumptionEntry, zoneId: ZoneId = ZoneId.systemDefault()) {
+ private fun ConsumptionEntry.toHydrationRecord(volumeMl: Double, zoneId: ZoneId): HydrationRecord {
+ val start = Instant.ofEpochMilli(startedAtMillis)
+ val end = start.plusSeconds(normalizedDurationMinutes * 60L)
+ val zoneRules = zoneId.rules
+ return HydrationRecord(
+ startTime = start,
+ endTime = end,
+ volume = Volume.liters(volumeMl / 1000.0),
+ startZoneOffset = zoneRules.getOffset(start),
+ endZoneOffset = zoneRules.getOffset(end),
+ metadata = HCMetadata.manualEntry(
+ clientRecordId = clientHydrationRecordId(id),
+ clientRecordVersion = System.currentTimeMillis(),
+ ),
+ )
+ }
+
+ suspend fun writeEntry(
+ entry: ConsumptionEntry,
+ zoneId: ZoneId = ZoneId.systemDefault(),
+ volumeMl: Double? = null,
+ ) {
val c = client ?: return
// Imported records are owned by another app — never echo them back
if (entry.healthConnectRecordId != null) return
if (!hasPermission()) return
- runCatching {
- c.insertRecords(listOf(entry.toNutritionRecord(zoneId)))
+ val records = mutableListOf(entry.toNutritionRecord(zoneId))
+ if (volumeMl != null && volumeMl > 0.0 && hasHydrationWritePermission()) {
+ records.add(entry.toHydrationRecord(volumeMl, zoneId))
}
+ runCatching { c.insertRecords(records) }
}
suspend fun syncAll(entries: List, zoneId: ZoneId = ZoneId.systemDefault()) {
@@ -129,6 +160,13 @@ class HealthConnectManager(private val context: Context) {
runCatching {
c.deleteRecords(NutritionRecord::class, emptyList(), clientIds)
}
+ // Also delete corresponding hydration records
+ if (hasHydrationWritePermission()) {
+ val hydrationClientIds = entryIds.map { clientHydrationRecordId(it) }
+ runCatching {
+ c.deleteRecords(HydrationRecord::class, emptyList(), hydrationClientIds)
+ }
+ }
}
suspend fun readForeignCaffeineRecords(
diff --git a/app/src/main/java/com/uc/caffeine/ui/viewmodel/CaffeineViewModel.kt b/app/src/main/java/com/uc/caffeine/ui/viewmodel/CaffeineViewModel.kt
index 9cc174c..f70a577 100644
--- a/app/src/main/java/com/uc/caffeine/ui/viewmodel/CaffeineViewModel.kt
+++ b/app/src/main/java/com/uc/caffeine/ui/viewmodel/CaffeineViewModel.kt
@@ -576,7 +576,8 @@ class CaffeineViewModel(application: Application) : AndroidViewModel(application
val settings = userSettings.value
if (settings.healthConnectEnabled) {
val zoneId = settings.resolvedZoneId()
- runCatching { healthConnectManager.writeEntry(entry.copy(id = newId.toInt()), zoneId) }
+ val volumeMl = defaultUnit.milliliters
+ runCatching { healthConnectManager.writeEntry(entry.copy(id = newId.toInt()), zoneId, volumeMl) }
}
}
}
@@ -602,7 +603,8 @@ class CaffeineViewModel(application: Application) : AndroidViewModel(application
val settings = userSettings.value
if (settings.healthConnectEnabled) {
val zoneId = settings.resolvedZoneId()
- runCatching { healthConnectManager.writeEntry(entry.copy(id = newId.toInt()), zoneId) }
+ val volumeMl = unit.milliliters?.let { it * quantity }
+ runCatching { healthConnectManager.writeEntry(entry.copy(id = newId.toInt()), zoneId, volumeMl) }
}
}
}
@@ -703,7 +705,8 @@ class CaffeineViewModel(application: Application) : AndroidViewModel(application
durationMinutes = coercedDuration,
)
val zoneId = settings.resolvedZoneId()
- runCatching { healthConnectManager.writeEntry(updatedEntry, zoneId) }
+ val volumeMl = unit.milliliters?.let { it * quantity }
+ runCatching { healthConnectManager.writeEntry(updatedEntry, zoneId, volumeMl) }
}
}
}