1 /**
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package com.android.healthconnect.controller.datasources.api
15 
16 import android.health.connect.HealthDataCategory
17 import com.android.healthconnect.controller.data.entries.api.ILoadDataAggregationsUseCase
18 import com.android.healthconnect.controller.data.entries.api.LoadAggregationInput
19 import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
20 import com.android.healthconnect.controller.datasources.AggregationCardInfo
21 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
22 import com.android.healthconnect.controller.service.IoDispatcher
23 import com.android.healthconnect.controller.shared.HealthDataCategoryInt
24 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
25 import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
26 import java.time.Instant
27 import java.time.LocalDate
28 import javax.inject.Inject
29 import javax.inject.Singleton
30 import kotlinx.coroutines.CoroutineDispatcher
31 import kotlinx.coroutines.withContext
32 
33 @Singleton
34 class LoadMostRecentAggregationsUseCase
35 @Inject
36 constructor(
37     private val loadDataAggregationsUseCase: ILoadDataAggregationsUseCase,
38     private val loadLastDateWithPriorityDataUseCase: ILoadLastDateWithPriorityDataUseCase,
39     private val sleepSessionHelper: ISleepSessionHelper,
40     @IoDispatcher private val dispatcher: CoroutineDispatcher,
41 ) : ILoadMostRecentAggregationsUseCase {
42 
43     /**
44      * Provides the most recent [AggregationDataCard]s info for Activity or Sleep.
45      *
46      * The latest aggregation always belongs to apps on the priority list. Apps not on the priority
47      * list do not contribute to aggregations or the last displayed date.
48      */
49     override suspend operator fun invoke(
50         healthDataCategory: @HealthDataCategoryInt Int
51     ): UseCaseResults<List<AggregationCardInfo>> =
52         withContext(dispatcher) {
53             try {
54                 val resultsList = mutableListOf<AggregationCardInfo>()
55                 if (healthDataCategory == HealthDataCategory.ACTIVITY) {
56                     val activityPermissionTypesWithAggregations =
57                         listOf(
58                             HealthPermissionType.STEPS,
59                             HealthPermissionType.DISTANCE,
60                             HealthPermissionType.TOTAL_CALORIES_BURNED)
61 
62                     activityPermissionTypesWithAggregations.forEach { permissionType ->
63                         val lastDateWithData: LocalDate?
64                         when (val lastDateWithDataResult =
65                             loadLastDateWithPriorityDataUseCase.invoke(permissionType)) {
66                             is UseCaseResults.Success -> {
67                                 lastDateWithData = lastDateWithDataResult.data
68                             }
69                             is UseCaseResults.Failed -> {
70                                 return@withContext UseCaseResults.Failed(
71                                     lastDateWithDataResult.exception)
72                             }
73                         }
74 
75                         val cardInfo =
76                             getLastAvailableActivityAggregation(lastDateWithData, permissionType)
77                         cardInfo?.let { resultsList.add(it) }
78                     }
79                 } else if (healthDataCategory == HealthDataCategory.SLEEP) {
80 
81                     val lastDateWithSleepData: LocalDate?
82                     when (val lastDateWithSleepDataResult =
83                         loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.SLEEP)) {
84                         is UseCaseResults.Success -> {
85                             lastDateWithSleepData = lastDateWithSleepDataResult.data
86                         }
87                         is UseCaseResults.Failed -> {
88                             return@withContext UseCaseResults.Failed(
89                                 lastDateWithSleepDataResult.exception)
90                         }
91                     }
92 
93                     val sleepCardInfo = getLastAvailableSleepAggregation(lastDateWithSleepData)
94                     sleepCardInfo?.let { resultsList.add(it) }
95                 }
96 
97                 UseCaseResults.Success(resultsList.toList())
98             } catch (e: Exception) {
99                 UseCaseResults.Failed(e)
100             }
101         }
102 
103     private suspend fun getLastAvailableActivityAggregation(
104         lastDateWithData: LocalDate?,
105         healthPermissionType: HealthPermissionType
106     ): AggregationCardInfo? {
107         if (lastDateWithData == null) {
108             return null
109         }
110 
111         // Get aggregate for last day
112         val lastDateInstant = lastDateWithData.toInstantAtStartOfDay()
113 
114         // call for aggregate
115         val input =
116             LoadAggregationInput.PeriodAggregation(
117                 permissionType = healthPermissionType,
118                 packageName = null,
119                 displayedStartTime = lastDateInstant,
120                 period = DateNavigationPeriod.PERIOD_DAY,
121                 showDataOrigin = false)
122 
123         return when (val useCaseResult = loadDataAggregationsUseCase.invoke(input)) {
124             is UseCaseResults.Success -> {
125                 // use this aggregation value to construct the card
126                 AggregationCardInfo(healthPermissionType, useCaseResult.data, lastDateInstant)
127             }
128             is UseCaseResults.Failed -> {
129                 throw useCaseResult.exception
130             }
131         }
132     }
133 
134     private suspend fun getLastAvailableSleepAggregation(
135         lastDateWithData: LocalDate?
136     ): AggregationCardInfo? {
137         if (lastDateWithData == null) {
138             return null
139         }
140 
141         when (val result = sleepSessionHelper.clusterSleepSessions(lastDateWithData)) {
142             is UseCaseResults.Success -> {
143                 result.data?.let { pair ->
144                     return computeSleepAggregation(pair.first, pair.second)
145                 }
146             }
147             is UseCaseResults.Failed -> {
148                 throw result.exception
149             }
150         }
151 
152         return null
153     }
154 
155     /**
156      * Returns an [AggregationCardInfo] representing the total sleep time from a list of sleep
157      * sessions starting on a particular day.
158      */
159     private suspend fun computeSleepAggregation(
160         minStartTime: Instant,
161         maxEndTime: Instant
162     ): AggregationCardInfo {
163         val aggregationInput =
164             LoadAggregationInput.CustomAggregation(
165                 permissionType = HealthPermissionType.SLEEP,
166                 packageName = null,
167                 startTime = minStartTime,
168                 endTime = maxEndTime,
169                 showDataOrigin = false)
170 
171         return when (val useCaseResult = loadDataAggregationsUseCase.invoke(aggregationInput)) {
172             is UseCaseResults.Success -> {
173                 // use this aggregation value to construct the card
174                 AggregationCardInfo(
175                     HealthPermissionType.SLEEP, useCaseResult.data, minStartTime, maxEndTime)
176             }
177             is UseCaseResults.Failed -> {
178                 throw useCaseResult.exception
179             }
180         }
181     }
182 }
183 
184 interface ILoadMostRecentAggregationsUseCase {
invokenull185     suspend fun invoke(
186         healthDataCategory: @HealthDataCategoryInt Int
187     ): UseCaseResults<List<AggregationCardInfo>>
188 }
189