<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