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