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