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