1 /** 2 * 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.data.entries 17 18 import android.util.Log 19 import androidx.lifecycle.LiveData 20 import androidx.lifecycle.MutableLiveData 21 import androidx.lifecycle.ViewModel 22 import androidx.lifecycle.viewModelScope 23 import com.android.healthconnect.controller.data.entries.api.ILoadDataAggregationsUseCase 24 import com.android.healthconnect.controller.data.entries.api.ILoadDataEntriesUseCase 25 import com.android.healthconnect.controller.data.entries.api.ILoadMenstruationDataUseCase 26 import com.android.healthconnect.controller.data.entries.api.LoadAggregationInput 27 import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput 28 import com.android.healthconnect.controller.data.entries.api.LoadMenstruationDataInput 29 import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod 30 import com.android.healthconnect.controller.permissions.data.HealthPermissionType 31 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.DISTANCE 32 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS 33 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.TOTAL_CALORIES_BURNED 34 import com.android.healthconnect.controller.shared.app.AppInfoReader 35 import com.android.healthconnect.controller.shared.app.AppMetadata 36 import com.android.healthconnect.controller.shared.usecase.UseCaseResults 37 import dagger.hilt.android.lifecycle.HiltViewModel 38 import java.time.Instant 39 import javax.inject.Inject 40 import kotlinx.coroutines.launch 41 42 /** View model for [AppEntriesFragment] and [AllEntriesFragment]. */ 43 @HiltViewModel 44 class EntriesViewModel 45 @Inject 46 constructor( 47 private val appInfoReader: AppInfoReader, 48 private val loadDataEntriesUseCase: ILoadDataEntriesUseCase, 49 private val loadMenstruationDataUseCase: ILoadMenstruationDataUseCase, 50 private val loadDataAggregationsUseCase: ILoadDataAggregationsUseCase 51 ) : ViewModel() { 52 53 companion object { 54 private const val TAG = "EntriesViewModel" 55 private val AGGREGATE_HEADER_DATA_TYPES = listOf(STEPS, DISTANCE, TOTAL_CALORIES_BURNED) 56 } 57 58 private val _entries = MutableLiveData<EntriesFragmentState>() 59 val entries: LiveData<EntriesFragmentState> 60 get() = _entries 61 62 val currentSelectedDate = MutableLiveData<Instant>() 63 val period = MutableLiveData<DateNavigationPeriod>() 64 65 private val _appInfo = MutableLiveData<AppMetadata>() 66 val appInfo: LiveData<AppMetadata> 67 get() = _appInfo 68 loadEntriesnull69 fun loadEntries( 70 permissionType: HealthPermissionType, 71 selectedDate: Instant, 72 period: DateNavigationPeriod 73 ) { 74 loadData(permissionType, packageName = null, selectedDate, period, showDataOrigin = true) 75 } 76 loadEntriesnull77 fun loadEntries( 78 permissionType: HealthPermissionType, 79 packageName: String, 80 selectedDate: Instant, 81 period: DateNavigationPeriod 82 ) { 83 loadData(permissionType, packageName, selectedDate, period, showDataOrigin = false) 84 } 85 loadDatanull86 private fun loadData( 87 permissionType: HealthPermissionType, 88 packageName: String?, 89 selectedDate: Instant, 90 period: DateNavigationPeriod, 91 showDataOrigin: Boolean 92 ) { 93 _entries.postValue(EntriesFragmentState.Loading) 94 currentSelectedDate.postValue(selectedDate) 95 this.period.postValue(period) 96 97 viewModelScope.launch { 98 val list = ArrayList<FormattedEntry>() 99 val entriesResults = 100 when (permissionType) { 101 // Special-casing Menstruation as it spans multiple days 102 HealthPermissionType.MENSTRUATION -> { 103 loadMenstruation(packageName, selectedDate, period, showDataOrigin) 104 } 105 else -> { 106 loadAppEntries( 107 permissionType, packageName, selectedDate, period, showDataOrigin) 108 } 109 } 110 when (entriesResults) { 111 is UseCaseResults.Success -> { 112 list.addAll(entriesResults.data) 113 if (list.isEmpty()) { 114 _entries.postValue(EntriesFragmentState.Empty) 115 } else { 116 addAggregation( 117 permissionType, packageName, selectedDate, period, list, showDataOrigin) 118 _entries.postValue(EntriesFragmentState.With(list)) 119 } 120 } 121 is UseCaseResults.Failed -> { 122 Log.e(TAG, "Loading error ", entriesResults.exception) 123 _entries.postValue(EntriesFragmentState.LoadingFailed) 124 } 125 } 126 } 127 } 128 loadAppEntriesnull129 private suspend fun loadAppEntries( 130 permissionType: HealthPermissionType, 131 packageName: String?, 132 selectedDate: Instant, 133 period: DateNavigationPeriod, 134 showDataOrigin: Boolean 135 ): UseCaseResults<List<FormattedEntry>> { 136 val input = 137 LoadDataEntriesInput(permissionType, packageName, selectedDate, period, showDataOrigin) 138 return loadDataEntriesUseCase.invoke(input) 139 } 140 loadMenstruationnull141 private suspend fun loadMenstruation( 142 packageName: String?, 143 selectedDate: Instant, 144 period: DateNavigationPeriod, 145 showDataOrigin: Boolean 146 ): UseCaseResults<List<FormattedEntry>> { 147 val input = LoadMenstruationDataInput(packageName, selectedDate, period, showDataOrigin) 148 return loadMenstruationDataUseCase.invoke(input) 149 } 150 loadAggregationnull151 private suspend fun loadAggregation( 152 permissionType: HealthPermissionType, 153 packageName: String?, 154 selectedDate: Instant, 155 period: DateNavigationPeriod, 156 showDataOrigin: Boolean 157 ): UseCaseResults<FormattedEntry.FormattedAggregation> { 158 val input = 159 LoadAggregationInput.PeriodAggregation( 160 permissionType, packageName, selectedDate, period, showDataOrigin) 161 return loadDataAggregationsUseCase.invoke(input) 162 } 163 loadAppInfonull164 fun loadAppInfo(packageName: String) { 165 viewModelScope.launch { _appInfo.postValue(appInfoReader.getAppMetadata(packageName)) } 166 } 167 addAggregationnull168 private suspend fun addAggregation( 169 permissionType: HealthPermissionType, 170 packageName: String?, 171 selectedDate: Instant, 172 period: DateNavigationPeriod, 173 list: ArrayList<FormattedEntry>, 174 showDataOrigin: Boolean 175 ) { 176 if (permissionType in AGGREGATE_HEADER_DATA_TYPES) { 177 when (val aggregationResult = 178 loadAggregation( 179 permissionType, packageName, selectedDate, period, showDataOrigin)) { 180 is UseCaseResults.Success -> { 181 list.add(0, aggregationResult.data) 182 } 183 is UseCaseResults.Failed -> { 184 Log.e(TAG, "Failed to load aggregation!", aggregationResult.exception) 185 } 186 } 187 } 188 } 189 190 sealed class EntriesFragmentState { 191 object Loading : EntriesFragmentState() 192 193 object Empty : EntriesFragmentState() 194 195 object LoadingFailed : EntriesFragmentState() 196 197 data class With(val entries: List<FormattedEntry>) : EntriesFragmentState() 198 } 199 } 200