• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  *
17  */
18 
19 package com.android.healthconnect.controller.dataentries
20 
21 import android.health.connect.HealthConnectManager
22 import android.health.connect.ReadRecordsRequestUsingFilters
23 import android.health.connect.ReadRecordsResponse
24 import android.health.connect.TimeInstantRangeFilter
25 import android.health.connect.datatypes.MenstruationFlowRecord
26 import android.health.connect.datatypes.MenstruationPeriodRecord
27 import android.health.connect.datatypes.Record
28 import android.util.Log
29 import androidx.core.os.asOutcomeReceiver
30 import com.android.healthconnect.controller.data.entries.FormattedEntry
31 import com.android.healthconnect.controller.dataentries.formatters.MenstruationPeriodFormatter
32 import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
33 import com.android.healthconnect.controller.service.IoDispatcher
34 import com.android.healthconnect.controller.shared.usecase.BaseUseCase
35 import java.time.Duration.ofDays
36 import java.time.Duration.ofHours
37 import java.time.Duration.ofMinutes
38 import java.time.Instant
39 import java.time.ZoneId
40 import javax.inject.Inject
41 import kotlinx.coroutines.CoroutineDispatcher
42 import kotlinx.coroutines.suspendCancellableCoroutine
43 
44 class LoadMenstruationDataUseCase
45 @Inject
46 constructor(
47     private val healthConnectManager: HealthConnectManager,
48     private val healthDataEntryFormatter: HealthDataEntryFormatter,
49     private val menstruationPeriodFormatter: MenstruationPeriodFormatter,
50     @IoDispatcher private val dispatcher: CoroutineDispatcher
51 ) : BaseUseCase<Instant, List<FormattedEntry>>(dispatcher) {
52 
53     companion object {
54         private const val TAG = "LoadMenstruationDataUse"
55         private val SEARCH_RANGE = ofDays(30)
56     }
57 
58     override suspend fun execute(input: Instant): List<FormattedEntry> {
59         val data = buildList {
60             addAll(getMenstruationPeriodRecords(input))
61             addAll(getMenstruationFlowRecords(input))
62         }
63         return data
64     }
65 
66     private suspend fun getMenstruationPeriodRecords(selectedDate: Instant): List<FormattedEntry> {
67         val startDate =
68             selectedDate
69                 .atZone(ZoneId.systemDefault())
70                 .toLocalDate()
71                 .atStartOfDay(ZoneId.systemDefault())
72                 .toInstant()
73         val end = startDate.plus(ofHours(23)).plus(ofMinutes(59))
74         val start = end.minus(SEARCH_RANGE)
75 
76         // Special-casing MenstruationPeriod as it spans multiple days and we show it on all these
77         // days in the UI (not just the first day).
78         // Hardcode max period length to 30 days (completely arbitrary number).
79         val timeRange = TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
80         val filter =
81             ReadRecordsRequestUsingFilters.Builder(MenstruationPeriodRecord::class.java)
82                 .setTimeRangeFilter(timeRange)
83                 .build()
84 
85         val records =
86             suspendCancellableCoroutine<ReadRecordsResponse<MenstruationPeriodRecord>> {
87                     continuation ->
88                     healthConnectManager.readRecords(
89                         filter, Runnable::run, continuation.asOutcomeReceiver())
90                 }
91                 .records
92                 .filter { menstruationPeriodRecord ->
93                     menstruationPeriodRecord.startTime.isBefore(end) &&
94                         (menstruationPeriodRecord.endTime.isAfter(startDate) ||
95                             menstruationPeriodRecord.endTime.equals(startDate))
96                 }
97 
98         return records.map { record -> menstruationPeriodFormatter.format(startDate, record) }
99     }
100 
101     private suspend fun getMenstruationFlowRecords(selectedDate: Instant): List<FormattedEntry> {
102         val start =
103             selectedDate
104                 .atZone(ZoneId.systemDefault())
105                 .toLocalDate()
106                 .atStartOfDay(ZoneId.systemDefault())
107                 .toInstant()
108         val end = start.plus(ofHours(23)).plus(ofMinutes(59))
109         val timeRange = TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
110         val filter =
111             ReadRecordsRequestUsingFilters.Builder(MenstruationFlowRecord::class.java)
112                 .setTimeRangeFilter(timeRange)
113                 .build()
114 
115         val records =
116             suspendCancellableCoroutine<ReadRecordsResponse<MenstruationFlowRecord>> { continuation
117                     ->
118                     healthConnectManager.readRecords(
119                         filter, Runnable::run, continuation.asOutcomeReceiver())
120                 }
121                 .records
122 
123         return records.mapNotNull { record -> getFormatterRecord(record) }
124     }
125 
126     private suspend fun getFormatterRecord(record: Record): FormattedEntry? {
127         return try {
128             healthDataEntryFormatter.format(record)
129         } catch (ex: Exception) {
130             Log.i(TAG, "Failed to format record!")
131             null
132         }
133     }
134 }
135