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