1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  *
17  */
18 
19 package com.android.healthconnect.controller.data.entries.api
20 
21 import android.health.connect.AggregateRecordsRequest
22 import android.health.connect.AggregateRecordsResponse
23 import android.health.connect.HealthConnectManager
24 import android.health.connect.TimeInstantRangeFilter
25 import android.health.connect.datatypes.AggregationType
26 import android.health.connect.datatypes.DataOrigin
27 import android.health.connect.datatypes.DistanceRecord
28 import android.health.connect.datatypes.SleepSessionRecord
29 import android.health.connect.datatypes.StepsRecord
30 import android.health.connect.datatypes.TotalCaloriesBurnedRecord
31 import android.health.connect.datatypes.units.Energy
32 import android.health.connect.datatypes.units.Length
33 import androidx.core.os.asOutcomeReceiver
34 import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
35 import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
36 import com.android.healthconnect.controller.dataentries.formatters.DistanceFormatter
37 import com.android.healthconnect.controller.dataentries.formatters.SleepSessionFormatter
38 import com.android.healthconnect.controller.dataentries.formatters.StepsFormatter
39 import com.android.healthconnect.controller.dataentries.formatters.TotalCaloriesBurnedFormatter
40 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
41 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.DISTANCE
42 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.SLEEP
43 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
44 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.TOTAL_CALORIES_BURNED
45 import com.android.healthconnect.controller.service.IoDispatcher
46 import com.android.healthconnect.controller.shared.app.AppInfoReader
47 import com.android.healthconnect.controller.shared.usecase.BaseUseCase
48 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
49 import java.time.Instant
50 import javax.inject.Inject
51 import javax.inject.Singleton
52 import kotlinx.coroutines.CoroutineDispatcher
53 import kotlinx.coroutines.suspendCancellableCoroutine
54 
55 /** Use case to load aggregation data on the Entries screens. */
56 @Singleton
57 class LoadDataAggregationsUseCase
58 @Inject
59 constructor(
60     private val loadEntriesHelper: LoadEntriesHelper,
61     private val stepsFormatter: StepsFormatter,
62     private val totalCaloriesBurnedFormatter: TotalCaloriesBurnedFormatter,
63     private val distanceFormatter: DistanceFormatter,
64     private val sleepSessionFormatter: SleepSessionFormatter,
65     private val healthConnectManager: HealthConnectManager,
66     private val appInfoReader: AppInfoReader,
67     @IoDispatcher private val dispatcher: CoroutineDispatcher
68 ) :
69     BaseUseCase<LoadAggregationInput, FormattedAggregation>(dispatcher),
70     ILoadDataAggregationsUseCase {
71 
72     override suspend fun execute(input: LoadAggregationInput): FormattedAggregation {
73         val timeFilterRange =
74             when (input) {
75                 is LoadAggregationInput.PeriodAggregation -> {
76                     loadEntriesHelper.getTimeFilter(
77                         input.displayedStartTime, input.period, endTimeExclusive = false)
78                 }
79                 is LoadAggregationInput.CustomAggregation -> {
80                     loadEntriesHelper.getTimeFilter(input.startTime, input.endTime)
81                 }
82             }
83         val showDataOrigin = input.showDataOrigin
84         val results =
85             when (input.permissionType) {
86                 STEPS -> {
87                     readAggregations<Long>(
88                         timeFilterRange,
89                         StepsRecord.STEPS_COUNT_TOTAL,
90                         input.packageName,
91                         showDataOrigin,
92                         input.permissionType)
93                 }
94                 DISTANCE -> {
95                     readAggregations<Length>(
96                         timeFilterRange,
97                         DistanceRecord.DISTANCE_TOTAL,
98                         input.packageName,
99                         showDataOrigin,
100                         input.permissionType)
101                 }
102                 TOTAL_CALORIES_BURNED -> {
103                     readAggregations<Energy>(
104                         timeFilterRange,
105                         TotalCaloriesBurnedRecord.ENERGY_TOTAL,
106                         input.packageName,
107                         showDataOrigin,
108                         input.permissionType)
109                 }
110                 SLEEP -> {
111                     readAggregations<Long>(
112                         timeFilterRange,
113                         SleepSessionRecord.SLEEP_DURATION_TOTAL,
114                         input.packageName,
115                         showDataOrigin,
116                         input.permissionType)
117                 }
118                 else ->
119                     throw IllegalArgumentException(
120                         "${input.permissionType} is not supported for aggregations!")
121             }
122 
123         return results
124     }
125 
126     private suspend fun <T> readAggregations(
127         timeFilterRange: TimeInstantRangeFilter,
128         aggregationType: AggregationType<T>,
129         packageName: String?,
130         showDataOrigin: Boolean,
131         healthPermissionType: HealthPermissionType
132     ): FormattedAggregation {
133         val request =
134             AggregateRecordsRequest.Builder<T>(timeFilterRange).addAggregationType(aggregationType)
135         if (packageName != null) {
136             request.addDataOriginsFilter(DataOrigin.Builder().setPackageName(packageName).build())
137         }
138 
139         val response =
140             suspendCancellableCoroutine<AggregateRecordsResponse<T>> { continuation ->
141                 healthConnectManager.aggregate(
142                     request.build(), Runnable::run, continuation.asOutcomeReceiver())
143             }
144         val aggregationResult: T = requireNotNull(response.get(aggregationType))
145         val apps = response.getDataOrigins(aggregationType)
146         return formatAggregation(aggregationResult, apps, showDataOrigin, healthPermissionType)
147     }
148 
149     private suspend fun <T> formatAggregation(
150         aggregationResult: T,
151         apps: Set<DataOrigin>,
152         showDataOrigin: Boolean,
153         healthPermissionType: HealthPermissionType
154     ): FormattedAggregation {
155         val contributingApps = getContributingApps(apps, showDataOrigin)
156         return when (aggregationResult) {
157             is Long -> {
158                 when (healthPermissionType) {
159                     STEPS ->
160                         FormattedAggregation(
161                             aggregation = stepsFormatter.formatUnit(aggregationResult),
162                             aggregationA11y = stepsFormatter.formatA11yUnit(aggregationResult),
163                             contributingApps = contributingApps)
164                     SLEEP ->
165                         FormattedAggregation(
166                             aggregation = sleepSessionFormatter.formatUnit(aggregationResult),
167                             aggregationA11y =
168                                 sleepSessionFormatter.formatA11yUnit(aggregationResult),
169                             contributingApps = contributingApps)
170                     else -> {
171                         throw IllegalArgumentException("Unsupported aggregation type!")
172                     }
173                 }
174             }
175             is Energy ->
176                 FormattedAggregation(
177                     aggregation = totalCaloriesBurnedFormatter.formatUnit(aggregationResult),
178                     aggregationA11y =
179                         totalCaloriesBurnedFormatter.formatA11yUnit(aggregationResult),
180                     contributingApps = contributingApps)
181             is Length ->
182                 FormattedAggregation(
183                     aggregation = distanceFormatter.formatUnit(aggregationResult),
184                     aggregationA11y = distanceFormatter.formatA11yUnit(aggregationResult),
185                     contributingApps = contributingApps)
186             else -> {
187                 throw IllegalArgumentException("Unsupported aggregation type!")
188             }
189         }
190     }
191 
192     private suspend fun getContributingApps(
193         apps: Set<DataOrigin>,
194         showDataOrigin: Boolean
195     ): String {
196         if (!showDataOrigin) {
197             return ""
198         }
199         return apps
200             .map { origin -> appInfoReader.getAppMetadata(origin.packageName) }
201             .joinToString(", ") { it.appName }
202     }
203 }
204 
205 sealed class LoadAggregationInput(
206     open val permissionType: HealthPermissionType,
207     open val packageName: String?,
208     open val showDataOrigin: Boolean
209 ) {
210     /** Aggregation input which uses a [DateNavigationPeriod] to calculate start and end times */
211     data class PeriodAggregation(
212         override val permissionType: HealthPermissionType,
213         override val packageName: String?,
214         val displayedStartTime: Instant,
215         val period: DateNavigationPeriod,
216         override val showDataOrigin: Boolean
217     ) : LoadAggregationInput(permissionType, packageName, showDataOrigin)
218 
219     /** Aggregation input with custom start and end times */
220     data class CustomAggregation(
221         override val permissionType: HealthPermissionType,
222         override val packageName: String?,
223         val startTime: Instant,
224         val endTime: Instant,
225         override val showDataOrigin: Boolean
226     ) : LoadAggregationInput(permissionType, packageName, showDataOrigin)
227 }
228 
229 interface ILoadDataAggregationsUseCase {
invokenull230     suspend fun invoke(input: LoadAggregationInput): UseCaseResults<FormattedAggregation>
231 
232     suspend fun execute(input: LoadAggregationInput): FormattedAggregation
233 }
234