• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.dataentries
20 
21 import android.content.Context
22 import android.health.connect.AggregateRecordsRequest
23 import android.health.connect.AggregateRecordsResponse
24 import android.health.connect.HealthConnectManager
25 import android.health.connect.TimeInstantRangeFilter
26 import android.health.connect.datatypes.AggregationType
27 import android.health.connect.datatypes.DataOrigin
28 import android.health.connect.datatypes.DistanceRecord
29 import android.health.connect.datatypes.SleepSessionRecord
30 import android.health.connect.datatypes.StepsRecord
31 import android.health.connect.datatypes.TotalCaloriesBurnedRecord
32 import android.health.connect.datatypes.units.Energy
33 import android.health.connect.datatypes.units.Length
34 import androidx.core.os.asOutcomeReceiver
35 import com.android.healthconnect.controller.R
36 import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
37 import com.android.healthconnect.controller.dataentries.formatters.DistanceFormatter
38 import com.android.healthconnect.controller.dataentries.formatters.SleepSessionFormatter
39 import com.android.healthconnect.controller.dataentries.formatters.StepsFormatter
40 import com.android.healthconnect.controller.dataentries.formatters.TotalCaloriesBurnedFormatter
41 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
42 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.DISTANCE
43 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.SLEEP
44 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
45 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.TOTAL_CALORIES_BURNED
46 import com.android.healthconnect.controller.service.IoDispatcher
47 import com.android.healthconnect.controller.shared.app.AppInfoReader
48 import com.android.healthconnect.controller.shared.usecase.BaseUseCase
49 import dagger.hilt.android.qualifiers.ApplicationContext
50 import java.time.Duration
51 import java.time.Instant
52 import java.time.ZoneId
53 import javax.inject.Inject
54 import javax.inject.Singleton
55 import kotlinx.coroutines.CoroutineDispatcher
56 import kotlinx.coroutines.suspendCancellableCoroutine
57 
58 @Singleton
59 class LoadDataAggregationsUseCase
60 @Inject
61 constructor(
62     private val stepsFormatter: StepsFormatter,
63     private val totalCaloriesBurnedFormatter: TotalCaloriesBurnedFormatter,
64     private val distanceFormatter: DistanceFormatter,
65     private val sleepFormatter: SleepSessionFormatter,
66     private val healthConnectManager: HealthConnectManager,
67     private val appInfoReader: AppInfoReader,
68     @IoDispatcher private val dispatcher: CoroutineDispatcher,
69     @ApplicationContext private val context: Context
70 ) : BaseUseCase<LoadAggregationInput, FormattedAggregation>(dispatcher) {
71 
72     override suspend fun execute(input: LoadAggregationInput): FormattedAggregation {
73         val timeFilterRange = getTimeFilter(input.selectedDate)
74         val results =
75             when (input.permissionType) {
76                 STEPS -> {
77                     readAggregations<Long>(
78                         timeFilterRange, StepsRecord.STEPS_COUNT_TOTAL, input.permissionType)
79                 }
80                 DISTANCE -> {
81                     readAggregations<Length>(
82                         timeFilterRange, DistanceRecord.DISTANCE_TOTAL, input.permissionType)
83                 }
84                 TOTAL_CALORIES_BURNED -> {
85                     readAggregations<Energy>(
86                         timeFilterRange,
87                         TotalCaloriesBurnedRecord.ENERGY_TOTAL,
88                         input.permissionType)
89                 }
90                 SLEEP -> {
91                     readAggregations<Long>(
92                         timeFilterRange,
93                         SleepSessionRecord.SLEEP_DURATION_TOTAL,
94                         input.permissionType)
95                 }
96                 else ->
97                     throw IllegalArgumentException(
98                         "${input.permissionType} is not supported for aggregations!")
99             }
100 
101         return results
102     }
103 
104     private suspend fun <T> readAggregations(
105         timeFilterRange: TimeInstantRangeFilter,
106         aggregationType: AggregationType<T>,
107         healthPermissionType: HealthPermissionType
108     ): FormattedAggregation {
109         val request =
110             AggregateRecordsRequest.Builder<T>(timeFilterRange)
111                 .addAggregationType(aggregationType)
112                 .build()
113 
114         val response =
115             suspendCancellableCoroutine<AggregateRecordsResponse<T>> { continuation ->
116                 healthConnectManager.aggregate(
117                     request, Runnable::run, continuation.asOutcomeReceiver())
118             }
119         val aggregationResult: T = requireNotNull(response.get(aggregationType))
120         val apps = response.getDataOrigins(aggregationType)
121         return formatAggregation(aggregationResult, apps, healthPermissionType)
122     }
123 
124     private suspend fun <T> formatAggregation(
125         aggregationResult: T,
126         apps: Set<DataOrigin>,
127         healthPermissionType: HealthPermissionType
128     ): FormattedAggregation {
129         val contributingApps = getContributingApps(apps)
130         return when (healthPermissionType) {
131             STEPS ->
132                 FormattedAggregation(
133                     aggregation = stepsFormatter.formatUnit(aggregationResult as Long),
134                     aggregationA11y =
135                         addAggregationA11yPrefix(stepsFormatter.formatA11yUnit(aggregationResult)),
136                     contributingApps = contributingApps)
137             TOTAL_CALORIES_BURNED ->
138                 FormattedAggregation(
139                     aggregation =
140                         totalCaloriesBurnedFormatter.formatUnit(aggregationResult as Energy),
141                     aggregationA11y =
142                         addAggregationA11yPrefix(
143                             totalCaloriesBurnedFormatter.formatA11yUnit(aggregationResult)),
144                     contributingApps = contributingApps)
145             DISTANCE ->
146                 FormattedAggregation(
147                     aggregation = distanceFormatter.formatUnit(aggregationResult as Length),
148                     aggregationA11y =
149                         addAggregationA11yPrefix(
150                             distanceFormatter.formatA11yUnit(aggregationResult)),
151                     contributingApps = contributingApps)
152             SLEEP ->
153                 FormattedAggregation(
154                     aggregation = sleepFormatter.formatUnit(aggregationResult as Long),
155                     aggregationA11y =
156                         addAggregationA11yPrefix(sleepFormatter.formatA11yUnit(aggregationResult)),
157                     contributingApps = contributingApps)
158             else -> {
159                 throw IllegalArgumentException("Unsupported aggregation type!")
160             }
161         }
162     }
163 
164     private fun addAggregationA11yPrefix(aggregation: String): String {
165         return context.getString(R.string.aggregation_total, aggregation)
166     }
167 
168     private suspend fun getContributingApps(apps: Set<DataOrigin>): String {
169         val separator: String = context.getString(R.string.data_type_separator)
170         return apps
171             .map { origin -> appInfoReader.getAppMetadata(origin.packageName) }
172             .joinToString(separator) { it.appName }
173     }
174 
175     private fun getTimeFilter(selectedDate: Instant): TimeInstantRangeFilter {
176         val start =
177             selectedDate
178                 .atZone(ZoneId.systemDefault())
179                 .toLocalDate()
180                 .atStartOfDay(ZoneId.systemDefault())
181                 .toInstant()
182         val end = start.plus(Duration.ofDays(1))
183         return TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
184     }
185 }
186 
187 data class LoadAggregationInput(
188     val permissionType: HealthPermissionType,
189     val selectedDate: Instant
190 )
191