<lambda>null1 package com.android.healthconnect.controller.datasources.api
2
3 import android.health.connect.datatypes.IntervalRecord
4 import android.health.connect.datatypes.Record
5 import android.health.connect.datatypes.SleepSessionRecord
6 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
7 import com.android.healthconnect.controller.service.IoDispatcher
8 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
9 import com.android.healthconnect.controller.utils.isAtLeastOneDayAfter
10 import com.android.healthconnect.controller.utils.isOnDayAfter
11 import com.android.healthconnect.controller.utils.isOnSameDay
12 import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
13 import com.android.healthconnect.controller.utils.toLocalDate
14 import com.google.common.collect.Comparators
15 import java.lang.Exception
16 import java.time.Instant
17 import java.time.LocalDate
18 import java.time.ZoneId
19 import javax.inject.Inject
20 import javax.inject.Singleton
21 import kotlinx.coroutines.CoroutineDispatcher
22 import kotlinx.coroutines.withContext
23
24 @Singleton
25 class SleepSessionHelper
26 @Inject
27 constructor(
28 private val loadPriorityEntriesUseCase: ILoadPriorityEntriesUseCase,
29 @IoDispatcher private val dispatcher: CoroutineDispatcher,
30 ) : ISleepSessionHelper {
31
32 /**
33 * Given a list of sleep session records starting on the last date with data, returns a pair of
34 * Instants representing a time interval [minStartTime, maxEndTime] between which we will query
35 * the aggregated time of sleep sessions.
36 */
37 override suspend fun clusterSleepSessions(
38 lastDateWithData: LocalDate
39 ): UseCaseResults<Pair<Instant, Instant>?> =
40 withContext(dispatcher) {
41 try {
42 val currentDaySleepData = getPrioritySleepRecords(lastDateWithData)
43
44 if (currentDaySleepData.isEmpty()) {
45 return@withContext UseCaseResults.Success(null)
46 }
47
48 // Determine if there is at least one session starting on Day 2 and finishing on Day
49 // 3
50 // (Case 3)
51 val sessionsCrossingMidnight =
52 currentDaySleepData.any { record ->
53 val currentSleepSession = (record as IntervalRecord)
54 (currentSleepSession.endTime.isAtLeastOneDayAfter(
55 currentSleepSession.startTime))
56 }
57
58 // Handle Case 3 - at least one sleep session starts on Day 2 and finishes on Day 3
59 if (sessionsCrossingMidnight) {
60 return@withContext UseCaseResults.Success(
61 handleSessionsCrossingMidnight(currentDaySleepData))
62 }
63
64 // case 1 - start and end times on the same day (Day 2)
65 // case 2 - there might be sessions starting on Day 1 and finishing on Day 2
66 // All sessions start and end on this day
67 // now we look at the date before to see if there is a session
68 // that ends today
69 val secondToLastDayWithData = lastDateWithData.minusDays(1)
70 val lastDateWithDataInstant = lastDateWithData.toInstantAtStartOfDay()
71
72 // Get all sleep sessions starting on secondToLastDate
73 val previousDaySleepData = getPrioritySleepRecords(secondToLastDayWithData)
74
75 // For each session check if the end date is last date
76 // If we find it, extend minStartTime to the start time of that session
77 // Case 1 - All sessions start and end on this day (Day 2)
78 // We also need these for case2
79 val minStartTime: Instant =
80 currentDaySleepData.minOf { (it as IntervalRecord).startTime }
81 val maxEndTime: Instant =
82 currentDaySleepData.maxOf { (it as IntervalRecord).endTime }
83
84 if (previousDaySleepData.isNotEmpty()) {
85 // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
86 return@withContext UseCaseResults.Success(
87 handleSessionsStartingOnSecondToLastDate(
88 previousDaySleepData,
89 lastDateWithDataInstant,
90 minStartTime,
91 maxEndTime))
92 }
93
94 return@withContext UseCaseResults.Success(Pair(minStartTime, maxEndTime))
95 } catch (e: Exception) {
96 return@withContext UseCaseResults.Failed(e)
97 }
98 }
99
100 /** Handles sleep session case 3 - At least one session crosses midnight into Day 3. */
101 private fun handleSessionsCrossingMidnight(entries: List<Record>): Pair<Instant, Instant> {
102 // We show aggregation for all sessions ending on day 3
103 // Find the max end time from all sessions crossing midnight
104 // and the min start time from all sessions that end on day 3
105 // There can be no session starting on day 3, otherwise that would be the latest date
106 var minStartTime: Instant = Instant.MAX
107 var maxEndTime: Instant = Instant.MIN
108
109 entries.forEach { record ->
110 val currentSleepSession = (record as IntervalRecord)
111 // Start day = Day 2
112 // We look at most 2 calendar days in the future, so the max possible end time
113 // is Day 4 at 12:00am
114 val maxPossibleEnd =
115 currentSleepSession.startTime
116 .toLocalDate()
117 .atStartOfDay(ZoneId.systemDefault())
118 .plusDays(2)
119 .toInstant()
120
121 if (currentSleepSession.endTime.isOnSameDay(currentSleepSession.startTime)) {
122 // This sleep session starts and ends on Day 2
123 // So we do not count this for either min or max
124 // As it belongs to the aggregations for Day 2
125 } else if (currentSleepSession.endTime.isOnDayAfter(currentSleepSession.startTime)) {
126 // This is a session [Day 2 - Day 3]
127 // min and max candidate
128 minStartTime = Comparators.min(minStartTime, currentSleepSession.startTime)
129 maxEndTime = Comparators.max(maxEndTime, currentSleepSession.endTime)
130 } else {
131 // currentSleepSession.endTime is further than Day 3
132 // Max End time should be Day 4 at 12am
133 minStartTime = Comparators.min(minStartTime, currentSleepSession.startTime)
134 maxEndTime = Comparators.max(maxEndTime, maxPossibleEnd)
135 }
136 }
137
138 return Pair(minStartTime, maxEndTime)
139 }
140
141 /**
142 * Handles sleep session Case 2 - At least one session starts on Day 1 and finishes on Day 2 or
143 * later.
144 */
145 private fun handleSessionsStartingOnSecondToLastDate(
146 previousDaySleepData: List<Record>,
147 lastDateWithDataInstant: Instant,
148 lastDayMinStartTime: Instant,
149 lastDayMaxEndTime: Instant
150 ): Pair<Instant, Instant> {
151
152 // This ensures we also take into account the sessions from lastDateWithData
153 var minStartTime = lastDayMinStartTime
154 var maxEndTime = lastDayMaxEndTime
155
156 previousDaySleepData.forEach { record ->
157 val currentSleepSession = (record as IntervalRecord)
158
159 // Start date is Day 1, so the max possible end date is Day 3 12am
160 val maxPossibleEnd =
161 currentSleepSession.startTime
162 .toLocalDate()
163 .atStartOfDay(ZoneId.systemDefault())
164 .plusDays(2)
165 .toInstant()
166
167 if (currentSleepSession.endTime.isOnSameDay(lastDateWithDataInstant)) {
168 // This is a sleep session that starts on Day 1 and finishes on Day 2
169 // min/max candidate
170 minStartTime = Comparators.min(minStartTime, currentSleepSession.startTime)
171 maxEndTime = Comparators.max(maxEndTime, currentSleepSession.endTime)
172 } else if (currentSleepSession.endTime.isOnSameDay(currentSleepSession.startTime)) {
173 // This is a sleep session that starts and ends on Day 1
174 // We do not count it for min/max because this belongs to Day 1
175 // aggregation
176 } else {
177 // This is a sleep session that start on Day 1 and ends after Day 2
178 // Then the max end time should be Day 3 at 12am
179 minStartTime = Comparators.min(minStartTime, currentSleepSession.startTime)
180 maxEndTime = Comparators.max(maxEndTime, maxPossibleEnd)
181 }
182 }
183
184 return Pair(minStartTime, maxEndTime)
185 }
186
187 /** Returns all priority sleep records starting on lastDateWithData. */
188 private suspend fun getPrioritySleepRecords(
189 lastDateWithData: LocalDate
190 ): List<SleepSessionRecord> {
191 when (val result =
192 loadPriorityEntriesUseCase.invoke(HealthPermissionType.SLEEP, lastDateWithData)) {
193 is UseCaseResults.Success -> {
194 return result.data.map { it as SleepSessionRecord }
195 }
196 is UseCaseResults.Failed -> {
197 throw result.exception
198 }
199 }
200 }
201 }
202
203 interface ISleepSessionHelper {
clusterSleepSessionsnull204 suspend fun clusterSleepSessions(
205 lastDateWithData: LocalDate
206 ): UseCaseResults<Pair<Instant, Instant>?>
207 }
208