diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/healthconnect/HealthConnectUtils.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/healthconnect/HealthConnectUtils.kt index 2a23698f5e..5380597d7b 100755 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/healthconnect/HealthConnectUtils.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/healthconnect/HealthConnectUtils.kt @@ -242,6 +242,7 @@ class HealthConnectUtils { val manager = GBApplication.app().deviceManager val syncIntervalInSeconds: Long = 24 * 60 * 60 // 1 day slices val lookBackInSeconds: Long = 24 * 60 * 60 + val sleepLookForwardSeconds: Long = 12 * 60 * 60 for (targetAddress in selectedDevices) { // Check if worker has been cancelled @@ -363,14 +364,19 @@ class HealthConnectUtils { updateSyncStatus(summary, true, summaryCallback, mainHandler) val queryStartTs = currentSliceStartTs.minusSeconds(lookBackInSeconds) - LOG.info("$HC_SYNC_TAG Querying Gadgetbridge DB for {}({}) from {} to {}", gbDevice.aliasOrName, dataType.name, queryStartTs, currentSliceEndTs) + val queryEndTs = if (dataType == HealthConnectPermissionManager.HealthConnectDataType.SLEEP) { + currentSliceEndTs.plusSeconds(sleepLookForwardSeconds) + } else { + currentSliceEndTs + } + LOG.info("$HC_SYNC_TAG Querying Gadgetbridge DB for {}({}) from {} to {}", gbDevice.aliasOrName, dataType.name, queryStartTs, queryEndTs) // Fetch activityBasedSamples under their own lock if needed for the current dataType val activityBasedSamples: List? = if (dataType == HealthConnectPermissionManager.HealthConnectDataType.ACTIVITY || dataType == HealthConnectPermissionManager.HealthConnectDataType.SLEEP) { GBApplication.acquireDbReadOnly().use { db -> - getActivitySamples(db, gbDevice, queryStartTs.epochSecond.toInt(), currentSliceEndTs.epochSecond.toInt()) + getActivitySamples(db, gbDevice, queryStartTs.epochSecond.toInt(), queryEndTs.epochSecond.toInt()) } } else { null diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/healthconnect/syncers/SleepSyncer.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/healthconnect/syncers/SleepSyncer.kt index 992f82d2b0..541d85971f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/healthconnect/syncers/SleepSyncer.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/healthconnect/syncers/SleepSyncer.kt @@ -123,11 +123,14 @@ internal object SleepSyncer : ContextualActivitySampleSyncer { val sessionBoundaryStart = analysisSession.sleepStart.toInstant() val sessionBoundaryEndInclusive = analysisSession.sleepEnd.toInstant() - // Check if session overlaps with the current slice (inclusive boundaries [sliceStart, sliceEnd]) - // Skip if: sessionStart > sliceEnd OR sessionEnd < sliceStart - if (sessionBoundaryStart.isAfter(sliceEndBoundary) || sessionBoundaryEndInclusive.isBefore(sliceStartBoundary)) { + // Only process this session if its START falls within the current slice [sliceStart, sliceEnd). + // The look-back query ensures full session data is available even for sessions starting near the + // previous slice boundary. The look-forward query ensures full session data for sessions starting + // near the current slice end. This ownership rule prevents duplicate records when the same session + // is discovered across multiple slices due to the look-back overlap. + if (sessionBoundaryStart.isBefore(sliceStartBoundary) || !sessionBoundaryStart.isBefore(sliceEndBoundary)) { LOG.debug( - "Skipping sleep session (identified by SleepAnalysis) for device '{}' (Timeframe: {} to {}) as it does not overlap with current slice ({} to {}).", + "Skipping sleep session (identified by SleepAnalysis) for device '{}' (Timeframe: {} to {}) as its start does not fall within current slice [{} to {}).", deviceName, sessionBoundaryStart, sessionBoundaryEndInclusive,