• 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.dataentries.formatters.MenstruationPeriodFormatter
31 import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
32 import com.android.healthconnect.controller.service.IoDispatcher
33 import com.android.healthconnect.controller.shared.usecase.BaseUseCase
34 import java.time.Duration.ofDays
35 import java.time.Duration.ofHours
36 import java.time.Duration.ofMinutes
37 import java.time.Instant
38 import java.time.ZoneId
39 import javax.inject.Inject
40 import kotlinx.coroutines.CoroutineDispatcher
41 import kotlinx.coroutines.suspendCancellableCoroutine
42 
43 class LoadMenstruationDataUseCase
44 @Inject
45 constructor(
46     private val healthConnectManager: HealthConnectManager,
47     private val healthDataEntryFormatter: HealthDataEntryFormatter,
48     private val menstruationPeriodFormatter: MenstruationPeriodFormatter,
49     @IoDispatcher private val dispatcher: CoroutineDispatcher
50 ) : BaseUseCase<Instant, List<FormattedEntry>>(dispatcher) {
51 
52     companion object {
53         private const val TAG = "LoadMenstruationDataUse"
54         private val SEARCH_RANGE = ofDays(30)
55     }
56 
57     override suspend fun execute(input: Instant): List<FormattedEntry> {
58         val data = buildList {
59             addAll(getMenstruationPeriodRecords(input))
60             addAll(getMenstruationFlowRecords(input))
61         }
62         return data
63     }
64 
65     private suspend fun getMenstruationPeriodRecords(selectedDate: Instant): List<FormattedEntry> {
66         val startDate =
67             selectedDate
68                 .atZone(ZoneId.systemDefault())
69                 .toLocalDate()
70                 .atStartOfDay(ZoneId.systemDefault())
71                 .toInstant()
72         val end = startDate.plus(ofHours(23)).plus(ofMinutes(59))
73         val start = end.minus(SEARCH_RANGE)
74 
75         // Special-casing MenstruationPeriod as it spans multiple days and we show it on all these
76         // days in the UI (not just the first day).
77         // Hardcode max period length to 30 days (completely arbitrary number).
78         val timeRange = TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
79         val filter =
80             ReadRecordsRequestUsingFilters.Builder(MenstruationPeriodRecord::class.java)
81                 .setTimeRangeFilter(timeRange)
82                 .build()
83 
84         val records =
85             suspendCancellableCoroutine<ReadRecordsResponse<MenstruationPeriodRecord>> {
86                     continuation ->
87                     healthConnectManager.readRecords(
88                         filter, Runnable::run, continuation.asOutcomeReceiver())
89                 }
90                 .records
91                 .filter { menstruationPeriodRecord ->
92                     menstruationPeriodRecord.startTime.isBefore(end) &&
93                         (menstruationPeriodRecord.endTime.isAfter(startDate) ||
94                             menstruationPeriodRecord.endTime.equals(startDate))
95                 }
96 
97         return records.map { record -> menstruationPeriodFormatter.format(startDate, record) }
98     }
99 
100     private suspend fun getMenstruationFlowRecords(selectedDate: Instant): List<FormattedEntry> {
101         val start =
102             selectedDate
103                 .atZone(ZoneId.systemDefault())
104                 .toLocalDate()
105                 .atStartOfDay(ZoneId.systemDefault())
106                 .toInstant()
107         val end = start.plus(ofHours(23)).plus(ofMinutes(59))
108         val timeRange = TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
109         val filter =
110             ReadRecordsRequestUsingFilters.Builder(MenstruationFlowRecord::class.java)
111                 .setTimeRangeFilter(timeRange)
112                 .build()
113 
114         val records =
115             suspendCancellableCoroutine<ReadRecordsResponse<MenstruationFlowRecord>> { continuation
116                     ->
117                     healthConnectManager.readRecords(
118                         filter, Runnable::run, continuation.asOutcomeReceiver())
119                 }
120                 .records
121 
122         return records.mapNotNull { record -> getFormatterRecord(record) }
123     }
124 
125     private suspend fun getFormatterRecord(record: Record): FormattedEntry? {
126         return try {
127             healthDataEntryFormatter.format(record)
128         } catch (ex: Exception) {
129             Log.i(TAG, "Failed to format record!")
130             null
131         }
132     }
133 }
134