1 /** <lambda>null2 * Copyright (C) 2023 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 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package com.android.healthconnect.controller.datasources 15 16 import android.health.connect.HealthDataCategory 17 import android.util.Log 18 import androidx.lifecycle.LiveData 19 import androidx.lifecycle.MediatorLiveData 20 import androidx.lifecycle.MutableLiveData 21 import androidx.lifecycle.ViewModel 22 import androidx.lifecycle.viewModelScope 23 import com.android.healthconnect.controller.datasources.api.ILoadMostRecentAggregationsUseCase 24 import com.android.healthconnect.controller.datasources.api.ILoadPotentialPriorityListUseCase 25 import com.android.healthconnect.controller.datasources.api.IUpdatePriorityListUseCase 26 import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase 27 import com.android.healthconnect.controller.shared.HealthDataCategoryInt 28 import com.android.healthconnect.controller.shared.app.AppInfoReader 29 import com.android.healthconnect.controller.shared.app.AppMetadata 30 import com.android.healthconnect.controller.shared.usecase.UseCaseResults 31 import dagger.hilt.android.lifecycle.HiltViewModel 32 import javax.inject.Inject 33 import kotlinx.coroutines.async 34 import kotlinx.coroutines.delay 35 import kotlinx.coroutines.launch 36 37 @HiltViewModel 38 class DataSourcesViewModel 39 @Inject 40 constructor( 41 private val loadDatesWithDataUseCase: ILoadMostRecentAggregationsUseCase, 42 private val loadPotentialAppSourcesUseCase: ILoadPotentialPriorityListUseCase, 43 private val loadPriorityListUseCase: ILoadPriorityListUseCase, 44 private val updatePriorityListUseCase: IUpdatePriorityListUseCase, 45 private val appInfoReader: AppInfoReader, 46 ) : ViewModel() { 47 48 companion object { 49 private const val TAG = "DataSourcesViewModel" 50 } 51 52 private val _aggregationCardsData = MutableLiveData<AggregationCardsState>() 53 54 private val _updatedAggregationCardsData = MutableLiveData<AggregationCardsState>() 55 56 // Used to control the reloading of the aggregation cards after reordering the priority list 57 // To avoid reloading the whole screen when only the cards need updating 58 // TODO (b/305907256) improve flow by observing the aggregationCardsData directly 59 val updatedAggregationCardsData: LiveData<AggregationCardsState> 60 get() = _updatedAggregationCardsData 61 62 private val _potentialAppSources = MutableLiveData<PotentialAppSourcesState>() 63 64 private val _shouldShowAddAnAppButton: MutableLiveData<Boolean> = MutableLiveData(false) 65 66 // Used to make sure Add an app button appears when removing an item from the priority list 67 val shouldShowAddAnAppButton: LiveData<Boolean> 68 get() = _shouldShowAddAnAppButton 69 70 private val _priorityListState = MutableLiveData<PriorityListState>() 71 72 private val _dataSourcesAndAggregationsInfo = MediatorLiveData<DataSourcesAndAggregationsInfo>() 73 val dataSourcesAndAggregationsInfo: LiveData<DataSourcesAndAggregationsInfo> 74 get() = _dataSourcesAndAggregationsInfo 75 76 private val _dataSourcesInfo = MediatorLiveData<DataSourcesInfo>() 77 val dataSourcesInfo: LiveData<DataSourcesInfo> 78 get() = _dataSourcesInfo 79 80 init { 81 _dataSourcesAndAggregationsInfo.addSource(_priorityListState) { priorityListState -> 82 if (!priorityListState.shouldObserve) { 83 return@addSource 84 } 85 _dataSourcesAndAggregationsInfo.value = 86 DataSourcesAndAggregationsInfo( 87 priorityListState = priorityListState, 88 potentialAppSourcesState = _potentialAppSources.value, 89 aggregationCardsState = _aggregationCardsData.value, 90 ) 91 } 92 _dataSourcesAndAggregationsInfo.addSource(_potentialAppSources) { potentialAppSourcesState 93 -> 94 if (!potentialAppSourcesState.shouldObserve) { 95 return@addSource 96 } 97 _dataSourcesAndAggregationsInfo.value = 98 DataSourcesAndAggregationsInfo( 99 priorityListState = _priorityListState.value, 100 potentialAppSourcesState = potentialAppSourcesState, 101 aggregationCardsState = _aggregationCardsData.value, 102 ) 103 } 104 _dataSourcesAndAggregationsInfo.addSource(_aggregationCardsData) { aggregationCardsState -> 105 if (!aggregationCardsState.shouldObserve) { 106 return@addSource 107 } 108 _dataSourcesAndAggregationsInfo.value = 109 DataSourcesAndAggregationsInfo( 110 priorityListState = _priorityListState.value, 111 potentialAppSourcesState = _potentialAppSources.value, 112 aggregationCardsState = aggregationCardsState, 113 ) 114 } 115 116 _dataSourcesInfo.addSource(_priorityListState) { priorityListState -> 117 _dataSourcesInfo.value = 118 DataSourcesInfo( 119 priorityListState = priorityListState, 120 potentialAppSourcesState = _potentialAppSources.value, 121 ) 122 } 123 124 _dataSourcesInfo.addSource(_potentialAppSources) { potentialAppSourcesState -> 125 _dataSourcesInfo.value = 126 DataSourcesInfo( 127 priorityListState = _priorityListState.value, 128 potentialAppSourcesState = potentialAppSourcesState, 129 ) 130 } 131 } 132 133 private var currentSelection = HealthDataCategory.ACTIVITY 134 135 fun getCurrentSelection(): Int = currentSelection 136 137 fun setCurrentSelection(category: @HealthDataCategoryInt Int) { 138 currentSelection = category 139 } 140 141 fun loadData(category: @HealthDataCategoryInt Int) { 142 loadMostRecentAggregations(category) 143 loadCurrentPriorityList(category) 144 loadPotentialAppSources(category) 145 } 146 147 private fun loadMostRecentAggregations(category: @HealthDataCategoryInt Int) { 148 _aggregationCardsData.postValue(AggregationCardsState.Loading(true)) 149 viewModelScope.launch { 150 when (val aggregationInfoResult = loadDatesWithDataUseCase.invoke(category)) { 151 is UseCaseResults.Success -> { 152 _aggregationCardsData.postValue( 153 AggregationCardsState.WithData(true, aggregationInfoResult.data) 154 ) 155 } 156 is UseCaseResults.Failed -> { 157 Log.e(TAG, "Failed loading dates with data ", aggregationInfoResult.exception) 158 _aggregationCardsData.postValue(AggregationCardsState.LoadingFailed(true)) 159 } 160 } 161 } 162 } 163 164 fun showAddAnAppButton() { 165 _shouldShowAddAnAppButton.postValue(true) 166 } 167 168 fun loadPotentialAppSources( 169 category: @HealthDataCategoryInt Int, 170 shouldObserve: Boolean = true, 171 ) { 172 _shouldShowAddAnAppButton.postValue(false) 173 _potentialAppSources.postValue(PotentialAppSourcesState.Loading(shouldObserve)) 174 viewModelScope.launch { 175 when (val appSourcesResult = loadPotentialAppSourcesUseCase.invoke(category)) { 176 is UseCaseResults.Success -> { 177 _potentialAppSources.postValue( 178 PotentialAppSourcesState.WithData(shouldObserve, appSourcesResult.data) 179 ) 180 } 181 is UseCaseResults.Failed -> { 182 Log.e( 183 TAG, 184 "Failed to load possible priority list candidates", 185 appSourcesResult.exception, 186 ) 187 _potentialAppSources.postValue( 188 PotentialAppSourcesState.LoadingFailed(shouldObserve) 189 ) 190 } 191 } 192 } 193 } 194 195 private fun loadCurrentPriorityList( 196 category: @HealthDataCategoryInt Int, 197 shouldObserve: Boolean = true, 198 ) { 199 _priorityListState.postValue(PriorityListState.Loading(shouldObserve)) 200 viewModelScope.launch { 201 when (val result = loadPriorityListUseCase.invoke(category)) { 202 is UseCaseResults.Success -> 203 _priorityListState.postValue( 204 if (result.data.isEmpty()) { 205 PriorityListState.WithData(shouldObserve, listOf()) 206 } else { 207 PriorityListState.WithData(shouldObserve, result.data) 208 } 209 ) 210 is UseCaseResults.Failed -> { 211 Log.e(TAG, "Load error ", result.exception) 212 _priorityListState.postValue(PriorityListState.LoadingFailed(shouldObserve)) 213 } 214 } 215 } 216 } 217 218 fun updatePriorityList(newPriorityList: List<String>, category: @HealthDataCategoryInt Int) { 219 _priorityListState.postValue(PriorityListState.Loading(false)) 220 viewModelScope.launch { 221 updatePriorityListUseCase.invoke(newPriorityList, category) 222 updateMostRecentAggregations(category) 223 val appMetadataList: List<AppMetadata> = 224 newPriorityList.map { appInfoReader.getAppMetadata(it) } 225 _priorityListState.postValue(PriorityListState.WithData(false, appMetadataList)) 226 } 227 } 228 229 private fun updateMostRecentAggregations(category: @HealthDataCategoryInt Int) { 230 _aggregationCardsData.postValue(AggregationCardsState.Loading(false)) 231 _updatedAggregationCardsData.postValue(AggregationCardsState.Loading(true)) 232 viewModelScope.launch { 233 val job = async { loadDatesWithDataUseCase.invoke(category) } 234 delay(1000) 235 236 when (val aggregationInfoResult = job.await()) { 237 is UseCaseResults.Success -> { 238 _aggregationCardsData.postValue( 239 AggregationCardsState.WithData(false, aggregationInfoResult.data) 240 ) 241 _updatedAggregationCardsData.postValue( 242 AggregationCardsState.WithData(true, aggregationInfoResult.data) 243 ) 244 } 245 is UseCaseResults.Failed -> { 246 Log.e(TAG, "Failed loading dates with data ", aggregationInfoResult.exception) 247 _aggregationCardsData.postValue(AggregationCardsState.LoadingFailed(false)) 248 _updatedAggregationCardsData.postValue( 249 AggregationCardsState.LoadingFailed(true) 250 ) 251 } 252 } 253 } 254 } 255 256 fun getPriorityList(): List<AppMetadata> = 257 when (val list = _priorityListState.value) { 258 is PriorityListState.WithData -> list.priorityList 259 else -> emptyList() 260 } 261 262 sealed class AggregationCardsState(open val shouldObserve: Boolean) { 263 data class Loading(override val shouldObserve: Boolean) : 264 AggregationCardsState(shouldObserve) 265 266 data class LoadingFailed(override val shouldObserve: Boolean) : 267 AggregationCardsState(shouldObserve) 268 269 data class WithData( 270 override val shouldObserve: Boolean, 271 val dataTotals: List<AggregationCardInfo>, 272 ) : AggregationCardsState(shouldObserve) 273 } 274 275 sealed class PotentialAppSourcesState(open val shouldObserve: Boolean) { 276 data class Loading(override val shouldObserve: Boolean) : 277 PotentialAppSourcesState(shouldObserve) 278 279 data class LoadingFailed(override val shouldObserve: Boolean) : 280 PotentialAppSourcesState(shouldObserve) 281 282 data class WithData( 283 override val shouldObserve: Boolean, 284 val appSources: List<AppMetadata>, 285 ) : PotentialAppSourcesState(shouldObserve) 286 } 287 288 sealed class PriorityListState(open val shouldObserve: Boolean) { 289 data class Loading(override val shouldObserve: Boolean) : PriorityListState(shouldObserve) 290 291 data class LoadingFailed(override val shouldObserve: Boolean) : 292 PriorityListState(shouldObserve) 293 294 data class WithData( 295 override val shouldObserve: Boolean, 296 val priorityList: List<AppMetadata>, 297 ) : PriorityListState(shouldObserve) 298 } 299 300 class DataSourcesInfo( 301 val priorityListState: PriorityListState?, 302 val potentialAppSourcesState: PotentialAppSourcesState?, 303 ) { 304 fun isLoading(): Boolean { 305 return priorityListState is PriorityListState.Loading || 306 potentialAppSourcesState is PotentialAppSourcesState.Loading 307 } 308 309 fun isLoadingFailed(): Boolean { 310 return priorityListState is PriorityListState.LoadingFailed || 311 potentialAppSourcesState is PotentialAppSourcesState.LoadingFailed 312 } 313 314 fun isWithData(): Boolean { 315 return priorityListState is PriorityListState.WithData && 316 potentialAppSourcesState is PotentialAppSourcesState.WithData 317 } 318 } 319 320 data class DataSourcesAndAggregationsInfo( 321 val priorityListState: PriorityListState?, 322 val potentialAppSourcesState: PotentialAppSourcesState?, 323 val aggregationCardsState: AggregationCardsState?, 324 ) { 325 fun isLoading(): Boolean { 326 return priorityListState is PriorityListState.Loading || 327 potentialAppSourcesState is PotentialAppSourcesState.Loading || 328 aggregationCardsState is AggregationCardsState.Loading 329 } 330 331 fun isLoadingFailed(): Boolean { 332 return priorityListState is PriorityListState.LoadingFailed || 333 potentialAppSourcesState is PotentialAppSourcesState.LoadingFailed || 334 aggregationCardsState is AggregationCardsState.LoadingFailed 335 } 336 337 fun isWithData(): Boolean { 338 return priorityListState is PriorityListState.WithData && 339 potentialAppSourcesState is PotentialAppSourcesState.WithData && 340 aggregationCardsState is AggregationCardsState.WithData 341 } 342 } 343 } 344