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