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