1 /**
<lambda>null2  * Copyright (C) 2022 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  * ```
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * ```
10  *
11  * Unless required by applicable law or agreed to in writing, software distributed under the License
12  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13  * or implied. See the License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package com.android.healthconnect.controller.dataentries
17 
18 import android.health.connect.HealthConnectManager
19 import android.health.connect.ReadRecordsRequestUsingFilters
20 import android.health.connect.ReadRecordsResponse
21 import android.health.connect.TimeInstantRangeFilter
22 import android.health.connect.datatypes.InstantRecord
23 import android.health.connect.datatypes.IntervalRecord
24 import android.health.connect.datatypes.Record
25 import android.util.Log
26 import androidx.core.os.asOutcomeReceiver
27 import com.android.healthconnect.controller.data.entries.FormattedEntry
28 import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
29 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
30 import com.android.healthconnect.controller.service.IoDispatcher
31 import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper.getDataTypes
32 import com.android.healthconnect.controller.shared.usecase.BaseUseCase
33 import java.time.Duration.ofHours
34 import java.time.Duration.ofMinutes
35 import java.time.Instant
36 import java.time.ZoneId
37 import javax.inject.Inject
38 import javax.inject.Singleton
39 import kotlinx.coroutines.CoroutineDispatcher
40 import kotlinx.coroutines.suspendCancellableCoroutine
41 
42 @Singleton
43 class LoadDataEntriesUseCase
44 @Inject
45 constructor(
46     private val healthDataEntryFormatter: HealthDataEntryFormatter,
47     private val healthConnectManager: HealthConnectManager,
48     @IoDispatcher private val dispatcher: CoroutineDispatcher
49 ) : BaseUseCase<LoadDataEntriesInput, List<FormattedEntry>>(dispatcher) {
50 
51     companion object {
52         private const val TAG = "LoadDataEntriesUseCase"
53     }
54 
55     override suspend fun execute(input: LoadDataEntriesInput): List<FormattedEntry> {
56         val timeFilterRange = getTimeFilter(input.selectedDate)
57         val dataTypes = getDataTypes(input.permissionType)
58         return dataTypes.map { dataType -> readDataType(dataType, timeFilterRange) }.flatten()
59     }
60 
61     private fun getTimeFilter(selectedDate: Instant): TimeInstantRangeFilter {
62         val start =
63             selectedDate
64                 .atZone(ZoneId.systemDefault())
65                 .toLocalDate()
66                 .atStartOfDay(ZoneId.systemDefault())
67                 .toInstant()
68         val end = start.plus(ofHours(23)).plus(ofMinutes(59))
69         return TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
70     }
71 
72     private suspend fun readDataType(
73         data: Class<out Record>,
74         timeFilterRange: TimeInstantRangeFilter
75     ): List<FormattedEntry> {
76         val filter =
77             ReadRecordsRequestUsingFilters.Builder(data).setTimeRangeFilter(timeFilterRange).build()
78         val records =
79             suspendCancellableCoroutine<ReadRecordsResponse<*>> { continuation ->
80                     healthConnectManager.readRecords(
81                         filter, Runnable::run, continuation.asOutcomeReceiver())
82                 }
83                 .records
84                 .sortedByDescending { record -> getStartTime(record) }
85         return records.mapNotNull { record -> getFormatterRecord(record) }
86     }
87 
88     private suspend fun getFormatterRecord(record: Record): FormattedEntry? {
89         return try {
90             healthDataEntryFormatter.format(record)
91         } catch (ex: Exception) {
92             Log.i(TAG, "Failed to format record!")
93             null
94         }
95     }
96 
97     private fun getStartTime(record: Record): Instant {
98         return when (record) {
99             is InstantRecord -> {
100                 record.time
101             }
102             is IntervalRecord -> {
103                 record.startTime
104             }
105             else -> {
106                 throw IllegalArgumentException("unsupported record type!")
107             }
108         }
109     }
110 }
111 
112 data class LoadDataEntriesInput(
113     val permissionType: HealthPermissionType,
114     val selectedDate: Instant
115 )
116