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.HealthConnectManager 17 import androidx.core.os.asOutcomeReceiver 18 import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput 19 import com.android.healthconnect.controller.data.entries.api.LoadEntriesHelper 20 import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod 21 import com.android.healthconnect.controller.permissions.data.HealthPermissionType 22 import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase 23 import com.android.healthconnect.controller.service.IoDispatcher 24 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.fromHealthPermissionType 25 import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper 26 import com.android.healthconnect.controller.shared.usecase.UseCaseResults 27 import com.android.healthconnect.controller.utils.TimeSource 28 import com.android.healthconnect.controller.utils.toInstantAtStartOfDay 29 import com.android.healthconnect.controller.utils.toLocalDate 30 import com.google.common.collect.Comparators.max 31 import java.time.LocalDate 32 import javax.inject.Inject 33 import javax.inject.Singleton 34 import kotlinx.coroutines.CoroutineDispatcher 35 import kotlinx.coroutines.suspendCancellableCoroutine 36 import kotlinx.coroutines.withContext 37 38 @Singleton 39 class LoadLastDateWithPriorityDataUseCase 40 @Inject 41 constructor( 42 private val healthConnectManager: HealthConnectManager, 43 private val loadEntriesHelper: LoadEntriesHelper, 44 private val loadPriorityListUseCase: ILoadPriorityListUseCase, 45 private val timeSource: TimeSource, 46 @IoDispatcher private val dispatcher: CoroutineDispatcher 47 ) : ILoadLastDateWithPriorityDataUseCase { 48 49 /** 50 * Returns the last local date with data for this health permission type, from the data owned by 51 * apps on the priority list. 52 */ 53 override suspend fun invoke( 54 healthPermissionType: HealthPermissionType 55 ): UseCaseResults<LocalDate?> = 56 withContext(dispatcher) { 57 var latestDateWithData: LocalDate? = null 58 try { 59 when (val priorityAppsResult = 60 loadPriorityListUseCase.invoke( 61 fromHealthPermissionType(healthPermissionType))) { 62 is UseCaseResults.Success -> { 63 val priorityApps = priorityAppsResult.data 64 65 priorityApps.forEach { priorityApp -> 66 val lastDateWithDataForApp = 67 loadLastDateWithDataForApp( 68 healthPermissionType, priorityApp.packageName) 69 70 latestDateWithData = 71 maxDateOrNull(latestDateWithData, lastDateWithDataForApp) 72 } 73 } 74 is UseCaseResults.Failed -> { 75 return@withContext UseCaseResults.Failed(priorityAppsResult.exception) 76 } 77 } 78 79 return@withContext UseCaseResults.Success(latestDateWithData) 80 } catch (e: Exception) { 81 UseCaseResults.Failed(e) 82 } 83 } 84 85 /** 86 * Returns the last date with data from a particular packageName, or null if no such date 87 * exists. 88 * 89 * To avoid querying all entries of all time, we first query for the activity dates for this 90 * healthPermissionType. We sort the dates in descending order and we find the first date which 91 * contains data from this packageName. 92 */ 93 private suspend fun loadLastDateWithDataForApp( 94 healthPermissionType: HealthPermissionType, 95 packageName: String 96 ): LocalDate? { 97 98 val recordTypes = HealthPermissionToDatatypeMapper.getDataTypes(healthPermissionType) 99 100 val datesWithData = suspendCancellableCoroutine { continuation -> 101 healthConnectManager.queryActivityDates( 102 recordTypes, Runnable::run, continuation.asOutcomeReceiver()) 103 } 104 105 val today = timeSource.currentLocalDateTime().toLocalDate() 106 val recentDates = 107 datesWithData.filter { date -> 108 date.isAfter(today.minusMonths(1)) && !date.isAfter(today) 109 } 110 111 // Activity dates are not kept during B&R, so it's possible to have data 112 // even without activity dates. 113 val minDate: LocalDate = 114 if (recentDates.isEmpty()) { 115 // Either there are no dates, or this is a fresh device out of D2D 116 // Check if there is any data in the past month anyway 117 today.minusMonths(1) 118 } else { 119 recentDates.min() 120 } 121 122 // Query the data entries from this last month in one single API call 123 val input = 124 LoadDataEntriesInput( 125 permissionType = healthPermissionType, 126 packageName = packageName, 127 displayedStartTime = minDate.toInstantAtStartOfDay(), 128 period = DateNavigationPeriod.PERIOD_MONTH, 129 showDataOrigin = false) 130 131 val entryRecords = loadEntriesHelper.readLastRecord(input) 132 133 if (entryRecords.isNotEmpty()) { 134 // The records are returned in descending order by startTime 135 return loadEntriesHelper.getStartTime(entryRecords[0]).toLocalDate() 136 } 137 138 return null 139 } 140 141 private fun maxDateOrNull(firstDate: LocalDate?, secondDate: LocalDate?): LocalDate? { 142 if (firstDate == null && secondDate == null) return null 143 if (firstDate == null) return secondDate 144 if (secondDate == null) return firstDate 145 146 return max(firstDate, secondDate) 147 } 148 } 149 150 interface ILoadLastDateWithPriorityDataUseCase { invokenull151 suspend fun invoke(healthPermissionType: HealthPermissionType): UseCaseResults<LocalDate?> 152 } 153