diff options
Diffstat (limited to 'apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt')
-rw-r--r-- | apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt | 309 |
1 files changed, 62 insertions, 247 deletions
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt b/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt index 12534aa4..4d202cfb 100644 --- a/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt +++ b/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt @@ -13,49 +13,39 @@ */ package com.android.healthconnect.controller.datasources.api -import android.health.connect.HealthConnectManager import android.health.connect.HealthDataCategory -import android.health.connect.datatypes.IntervalRecord -import android.health.connect.datatypes.Record -import androidx.core.os.asOutcomeReceiver import com.android.healthconnect.controller.data.entries.api.ILoadDataAggregationsUseCase -import com.android.healthconnect.controller.data.entries.api.ILoadSleepDataUseCase import com.android.healthconnect.controller.data.entries.api.LoadAggregationInput -import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod import com.android.healthconnect.controller.datasources.AggregationCardInfo import com.android.healthconnect.controller.permissions.data.HealthPermissionType import com.android.healthconnect.controller.service.IoDispatcher import com.android.healthconnect.controller.shared.HealthDataCategoryInt -import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper import com.android.healthconnect.controller.shared.usecase.UseCaseResults -import com.android.healthconnect.controller.utils.atStartOfDay -import com.android.healthconnect.controller.utils.isAtLeastOneDayAfter -import com.android.healthconnect.controller.utils.isOnDayAfter -import com.android.healthconnect.controller.utils.isOnSameDay import com.android.healthconnect.controller.utils.toInstantAtStartOfDay -import com.android.healthconnect.controller.utils.toLocalDate -import com.google.common.collect.Comparators.max -import com.google.common.collect.Comparators.min import java.time.Instant import java.time.LocalDate -import java.time.ZoneId import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @Singleton class LoadMostRecentAggregationsUseCase @Inject constructor( - private val healthConnectManager: HealthConnectManager, private val loadDataAggregationsUseCase: ILoadDataAggregationsUseCase, - private val loadSleepDataUseCase: ILoadSleepDataUseCase, + private val loadLastDateWithPriorityDataUseCase: ILoadLastDateWithPriorityDataUseCase, + private val sleepSessionHelper: ISleepSessionHelper, @IoDispatcher private val dispatcher: CoroutineDispatcher, ) : ILoadMostRecentAggregationsUseCase { - /** Invoked to provide [AggregationDataCard]s info for Activity and Sleep */ + + /** + * Provides the most recent [AggregationDataCard]s info for Activity or Sleep. + * + * The latest aggregation always belongs to apps on the priority list. Apps not on the priority + * list do not contribute to aggregations or the last displayed date. + */ override suspend operator fun invoke( healthDataCategory: @HealthDataCategoryInt Int ): UseCaseResults<List<AggregationCardInfo>> = @@ -63,59 +53,45 @@ constructor( try { val resultsList = mutableListOf<AggregationCardInfo>() if (healthDataCategory == HealthDataCategory.ACTIVITY) { - val stepsRecordTypes = - HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.STEPS) - val datesWithStepsData = suspendCancellableCoroutine { continuation -> - healthConnectManager.queryActivityDates( - stepsRecordTypes, Runnable::run, continuation.asOutcomeReceiver()) - } - - if (datesWithStepsData.isNotEmpty()) { - val stepsCardInfo = - getLastAvailableAggregation( - datesWithStepsData, HealthPermissionType.STEPS) - stepsCardInfo?.let { resultsList.add(it) } - } - - val distanceRecordTypes = - HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.DISTANCE) - val datesWithDistanceData = suspendCancellableCoroutine { continuation -> - healthConnectManager.queryActivityDates( - distanceRecordTypes, Runnable::run, continuation.asOutcomeReceiver()) - } - - if (datesWithDistanceData.isNotEmpty()) { - val distanceCardInfo = - getLastAvailableAggregation( - datesWithDistanceData, HealthPermissionType.DISTANCE) - distanceCardInfo?.let { resultsList.add(it) } - } - - val caloriesRecordTypes = - HealthPermissionToDatatypeMapper.getDataTypes( + val activityPermissionTypesWithAggregations = + listOf( + HealthPermissionType.STEPS, + HealthPermissionType.DISTANCE, HealthPermissionType.TOTAL_CALORIES_BURNED) - val datesWithCaloriesData = suspendCancellableCoroutine { continuation -> - healthConnectManager.queryActivityDates( - caloriesRecordTypes, Runnable::run, continuation.asOutcomeReceiver()) - } - if (datesWithCaloriesData.isNotEmpty()) { - val caloriesCardInfo = - getLastAvailableAggregation( - datesWithCaloriesData, HealthPermissionType.TOTAL_CALORIES_BURNED) - caloriesCardInfo?.let { resultsList.add(it) } + activityPermissionTypesWithAggregations.forEach { permissionType -> + val lastDateWithData: LocalDate? + when (val lastDateWithDataResult = + loadLastDateWithPriorityDataUseCase.invoke(permissionType)) { + is UseCaseResults.Success -> { + lastDateWithData = lastDateWithDataResult.data + } + is UseCaseResults.Failed -> { + return@withContext UseCaseResults.Failed( + lastDateWithDataResult.exception) + } + } + + val cardInfo = + getLastAvailableActivityAggregation(lastDateWithData, permissionType) + cardInfo?.let { resultsList.add(it) } } } else if (healthDataCategory == HealthDataCategory.SLEEP) { - val sleepRecordTypes = - HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.SLEEP) - val datesWithSleepData = suspendCancellableCoroutine { continuation -> - healthConnectManager.queryActivityDates( - sleepRecordTypes, Runnable::run, continuation.asOutcomeReceiver()) - } - if (datesWithSleepData.isNotEmpty()) { - val sleepCardInfo = getLastAvailableSleepAggregation(datesWithSleepData) - sleepCardInfo?.let { resultsList.add(it) } + + val lastDateWithSleepData: LocalDate? + when (val lastDateWithSleepDataResult = + loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.SLEEP)) { + is UseCaseResults.Success -> { + lastDateWithSleepData = lastDateWithSleepDataResult.data + } + is UseCaseResults.Failed -> { + return@withContext UseCaseResults.Failed( + lastDateWithSleepDataResult.exception) + } } + + val sleepCardInfo = getLastAvailableSleepAggregation(lastDateWithSleepData) + sleepCardInfo?.let { resultsList.add(it) } } UseCaseResults.Success(resultsList.toList()) @@ -124,13 +100,16 @@ constructor( } } - private suspend fun getLastAvailableAggregation( - datesWithData: List<LocalDate>, + private suspend fun getLastAvailableActivityAggregation( + lastDateWithData: LocalDate?, healthPermissionType: HealthPermissionType ): AggregationCardInfo? { + if (lastDateWithData == null) { + return null + } + // Get aggregate for last day - val lastDate = datesWithData.maxOf { it } - val lastDateInstant = lastDate.atStartOfDay(ZoneId.systemDefault()).toInstant() + val lastDateInstant = lastDateWithData.toInstantAtStartOfDay() // call for aggregate val input = @@ -147,190 +126,30 @@ constructor( AggregationCardInfo(healthPermissionType, useCaseResult.data, lastDateInstant) } is UseCaseResults.Failed -> { - // Something went wrong here, so return nothing - null + throw useCaseResult.exception } } } private suspend fun getLastAvailableSleepAggregation( - datesWithData: List<LocalDate> + lastDateWithData: LocalDate? ): AggregationCardInfo? { - // Get last date with data (the start date of sleep sessions) - val lastDateWithData = datesWithData.last() - val lastDateInstant = lastDateWithData.toInstantAtStartOfDay() - - // Get all sleep sessions starting on that date - val input = - LoadDataEntriesInput( - HealthPermissionType.SLEEP, - packageName = null, - displayedStartTime = lastDateInstant, - period = DateNavigationPeriod.PERIOD_DAY, - showDataOrigin = false) - - return when (val result = loadSleepDataUseCase.invoke(input)) { - is UseCaseResults.Success -> { - val sleepRecords = result.data - val (minStartTime, maxEndTime) = - clusterSleepSessions(sleepRecords, lastDateWithData) - computeSleepAggregation(minStartTime, maxEndTime) - } - is UseCaseResults.Failed -> { - null - } - } - } - - /** - * Given a list of sleep session records starting on the last date with data, returns a pair of - * Instants representing a time interval [minStartTime, maxEndTime] between which we will query - * the aggregated time of sleep sessions. - */ - private suspend fun clusterSleepSessions( - entries: List<Record>, - lastDateWithData: LocalDate - ): Pair<Instant, Instant> { - - var minStartTime: Instant = Instant.MAX - var maxEndTime: Instant = Instant.MIN - - // Determine if there is at least one session starting on Day 2 and finishing on Day 3 - // (Case 3) - val sessionsCrossingMidnight = - entries.any { record -> - val currentSleepSession = (record as IntervalRecord) - (currentSleepSession.endTime.isAtLeastOneDayAfter(currentSleepSession.startTime)) - } - - // Handle Case 3 - at least one sleep session starts on Day 2 and finishes on Day 3 - if (sessionsCrossingMidnight) { - return handleSessionsCrossingMidnight(entries) + if (lastDateWithData == null) { + return null } - // case 1 - start and end times on the same day (Day 2) - // case 2 - there might be sessions starting on Day 1 and finishing on Day 2 - // All sessions start and end on this day - // now we look at the date before to see if there is a session - // that ends today - val secondToLastDateInstant = - lastDateWithData.minusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant() - val lastDateWithDataInstant = lastDateWithData.toInstantAtStartOfDay() - - // Get all sleep sessions starting on secondToLastDate - val input = - LoadDataEntriesInput( - HealthPermissionType.SLEEP, - packageName = null, - displayedStartTime = secondToLastDateInstant, - period = DateNavigationPeriod.PERIOD_DAY, - showDataOrigin = false) - - when (val result = loadSleepDataUseCase.invoke(input)) { + when (val result = sleepSessionHelper.clusterSleepSessions(lastDateWithData)) { is UseCaseResults.Success -> { - val previousDaySleepData = result.data - // For each session check if the end date is last date - // If we find it, extend minStartTime to the start time of that session - - if (previousDaySleepData.isEmpty()) { - // Case 1 - All sessions start and end on this day (Day 2) - minStartTime = entries.minOf { (it as IntervalRecord).startTime } - maxEndTime = entries.maxOf { (it as IntervalRecord).endTime } - } else { - // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later - return handleSessionsStartingOnSecondToLastDate( - previousDaySleepData, lastDateWithDataInstant) + result.data?.let { pair -> + return computeSleepAggregation(pair.first, pair.second) } } is UseCaseResults.Failed -> { - Pair(Instant.MAX, Instant.MAX) - } - } - - return Pair(minStartTime, maxEndTime) - } - - /** Handles sleep session case 3 - At least one session crosses midnight into Day 3. */ - private fun handleSessionsCrossingMidnight(entries: List<Record>): Pair<Instant, Instant> { - // We show aggregation for all sessions ending on day 3 - // Find the max end time from all sessions crossing midnight - // and the min start time from all sessions that end on day 3 - // There can be no session starting on day 3, otherwise that would be the latest date - var minStartTime: Instant = Instant.MAX - var maxEndTime: Instant = Instant.MIN - - entries.forEach { record -> - val currentSleepSession = (record as IntervalRecord) - // Start day = Day 2 - // We look at most 2 calendar days in the future, so the max possible end time - // is Day 4 at 12:00am - val maxPossibleEnd = - currentSleepSession.startTime - .toLocalDate() - .atStartOfDay(ZoneId.systemDefault()) - .plusDays(2) - .toInstant() - - if (currentSleepSession.endTime.isOnSameDay(currentSleepSession.startTime)) { - // This sleep session starts and ends on Day 2 - // So we do not count this for either min or max - // As it belongs to the aggregations for Day 2 - } else if (currentSleepSession.endTime.isOnDayAfter(currentSleepSession.startTime)) { - // This is a session [Day 2 - Day 3] - // min and max candidate - minStartTime = min(minStartTime, currentSleepSession.startTime) - maxEndTime = max(maxEndTime, currentSleepSession.endTime) - } else { - // currentSleepSession.endTime is further than Day 3 - // Max End time should be Day 4 at 12am - minStartTime = min(minStartTime, currentSleepSession.startTime) - maxEndTime = max(maxEndTime, maxPossibleEnd) - } - } - - return Pair(minStartTime, maxEndTime) - } - - /** - * Handles sleep session Case 2 - At least one session starts on Day 1 and finishes on Day 2 or - * later. - */ - private fun handleSessionsStartingOnSecondToLastDate( - previousDaySleepData: List<Record>, - lastDateWithDataInstant: Instant - ): Pair<Instant, Instant> { - var minStartTime: Instant = Instant.MAX - var maxEndTime: Instant = Instant.MIN - - previousDaySleepData.forEach { record -> - val currentSleepSession = (record as IntervalRecord) - - // Start date is Day 1, so the max possible end date is Day 3 12am - val maxPossibleEnd = - currentSleepSession.startTime - .toLocalDate() - .atStartOfDay(ZoneId.systemDefault()) - .plusDays(2) - .toInstant() - - if (currentSleepSession.endTime.isOnSameDay(lastDateWithDataInstant)) { - // This is a sleep session that starts on Day 1 and finishes on Day 2 - // min/max candidate - minStartTime = min(minStartTime, currentSleepSession.startTime) - maxEndTime = max(maxEndTime, currentSleepSession.endTime) - } else if (currentSleepSession.endTime.isOnSameDay(currentSleepSession.startTime)) { - // This is a sleep session that starts and ends on Day 1 - // We do not count it for min/max because this belongs to Day 1 - // aggregation - } else { - // This is a sleep session that start on Day 1 and ends after Day 2 - // Then the max end time should be Day 3 at 12am - minStartTime = min(minStartTime, currentSleepSession.startTime) - maxEndTime = max(maxEndTime, maxPossibleEnd) + throw result.exception } } - return Pair(minStartTime, maxEndTime) + return null } /** @@ -340,7 +159,7 @@ constructor( private suspend fun computeSleepAggregation( minStartTime: Instant, maxEndTime: Instant - ): AggregationCardInfo? { + ): AggregationCardInfo { val aggregationInput = LoadAggregationInput.CustomAggregation( permissionType = HealthPermissionType.SLEEP, @@ -353,14 +172,10 @@ constructor( is UseCaseResults.Success -> { // use this aggregation value to construct the card AggregationCardInfo( - HealthPermissionType.SLEEP, - useCaseResult.data, - minStartTime.atStartOfDay(), - maxEndTime.atStartOfDay()) + HealthPermissionType.SLEEP, useCaseResult.data, minStartTime, maxEndTime) } is UseCaseResults.Failed -> { - // Something went wrong here, so return nothing - null + throw useCaseResult.exception } } } |