/** * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.android.healthconnect.controller.datasources import android.health.connect.HealthDataCategory import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.healthconnect.controller.datasources.api.ILoadMostRecentAggregationsUseCase import com.android.healthconnect.controller.datasources.api.ILoadPotentialPriorityListUseCase import com.android.healthconnect.controller.datasources.api.IUpdatePriorityListUseCase import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase import com.android.healthconnect.controller.shared.HealthDataCategoryInt import com.android.healthconnect.controller.shared.app.AppInfoReader import com.android.healthconnect.controller.shared.app.AppMetadata import com.android.healthconnect.controller.shared.usecase.UseCaseResults import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch @HiltViewModel class DataSourcesViewModel @Inject constructor( private val loadDatesWithDataUseCase: ILoadMostRecentAggregationsUseCase, private val loadPotentialAppSourcesUseCase: ILoadPotentialPriorityListUseCase, private val loadPriorityListUseCase: ILoadPriorityListUseCase, private val updatePriorityListUseCase: IUpdatePriorityListUseCase, private val appInfoReader: AppInfoReader, ) : ViewModel() { companion object { private const val TAG = "DataSourcesViewModel" } private val _aggregationCardsData = MutableLiveData() private val _updatedAggregationCardsData = MutableLiveData() // Used to control the reloading of the aggregation cards after reordering the priority list // To avoid reloading the whole screen when only the cards need updating // TODO (b/305907256) improve flow by observing the aggregationCardsData directly val updatedAggregationCardsData: LiveData get() = _updatedAggregationCardsData private val _potentialAppSources = MutableLiveData() private val _shouldShowAddAnAppButton: MutableLiveData = MutableLiveData(false) // Used to make sure Add an app button appears when removing an item from the priority list val shouldShowAddAnAppButton: LiveData get() = _shouldShowAddAnAppButton private val _priorityListState = MutableLiveData() private val _dataSourcesAndAggregationsInfo = MediatorLiveData() val dataSourcesAndAggregationsInfo: LiveData get() = _dataSourcesAndAggregationsInfo private val _dataSourcesInfo = MediatorLiveData() val dataSourcesInfo: LiveData get() = _dataSourcesInfo init { _dataSourcesAndAggregationsInfo.addSource(_priorityListState) { priorityListState -> if (!priorityListState.shouldObserve) { return@addSource } _dataSourcesAndAggregationsInfo.value = DataSourcesAndAggregationsInfo( priorityListState = priorityListState, potentialAppSourcesState = _potentialAppSources.value, aggregationCardsState = _aggregationCardsData.value, ) } _dataSourcesAndAggregationsInfo.addSource(_potentialAppSources) { potentialAppSourcesState -> if (!potentialAppSourcesState.shouldObserve) { return@addSource } _dataSourcesAndAggregationsInfo.value = DataSourcesAndAggregationsInfo( priorityListState = _priorityListState.value, potentialAppSourcesState = potentialAppSourcesState, aggregationCardsState = _aggregationCardsData.value, ) } _dataSourcesAndAggregationsInfo.addSource(_aggregationCardsData) { aggregationCardsState -> if (!aggregationCardsState.shouldObserve) { return@addSource } _dataSourcesAndAggregationsInfo.value = DataSourcesAndAggregationsInfo( priorityListState = _priorityListState.value, potentialAppSourcesState = _potentialAppSources.value, aggregationCardsState = aggregationCardsState, ) } _dataSourcesInfo.addSource(_priorityListState) { priorityListState -> _dataSourcesInfo.value = DataSourcesInfo( priorityListState = priorityListState, potentialAppSourcesState = _potentialAppSources.value, ) } _dataSourcesInfo.addSource(_potentialAppSources) { potentialAppSourcesState -> _dataSourcesInfo.value = DataSourcesInfo( priorityListState = _priorityListState.value, potentialAppSourcesState = potentialAppSourcesState, ) } } private var currentSelection = HealthDataCategory.ACTIVITY fun getCurrentSelection(): Int = currentSelection fun setCurrentSelection(category: @HealthDataCategoryInt Int) { currentSelection = category } fun loadData(category: @HealthDataCategoryInt Int) { loadMostRecentAggregations(category) loadCurrentPriorityList(category) loadPotentialAppSources(category) } private fun loadMostRecentAggregations(category: @HealthDataCategoryInt Int) { _aggregationCardsData.postValue(AggregationCardsState.Loading(true)) viewModelScope.launch { when (val aggregationInfoResult = loadDatesWithDataUseCase.invoke(category)) { is UseCaseResults.Success -> { _aggregationCardsData.postValue( AggregationCardsState.WithData(true, aggregationInfoResult.data) ) } is UseCaseResults.Failed -> { Log.e(TAG, "Failed loading dates with data ", aggregationInfoResult.exception) _aggregationCardsData.postValue(AggregationCardsState.LoadingFailed(true)) } } } } fun showAddAnAppButton() { _shouldShowAddAnAppButton.postValue(true) } fun loadPotentialAppSources( category: @HealthDataCategoryInt Int, shouldObserve: Boolean = true, ) { _shouldShowAddAnAppButton.postValue(false) _potentialAppSources.postValue(PotentialAppSourcesState.Loading(shouldObserve)) viewModelScope.launch { when (val appSourcesResult = loadPotentialAppSourcesUseCase.invoke(category)) { is UseCaseResults.Success -> { _potentialAppSources.postValue( PotentialAppSourcesState.WithData(shouldObserve, appSourcesResult.data) ) } is UseCaseResults.Failed -> { Log.e( TAG, "Failed to load possible priority list candidates", appSourcesResult.exception, ) _potentialAppSources.postValue( PotentialAppSourcesState.LoadingFailed(shouldObserve) ) } } } } private fun loadCurrentPriorityList( category: @HealthDataCategoryInt Int, shouldObserve: Boolean = true, ) { _priorityListState.postValue(PriorityListState.Loading(shouldObserve)) viewModelScope.launch { when (val result = loadPriorityListUseCase.invoke(category)) { is UseCaseResults.Success -> _priorityListState.postValue( if (result.data.isEmpty()) { PriorityListState.WithData(shouldObserve, listOf()) } else { PriorityListState.WithData(shouldObserve, result.data) } ) is UseCaseResults.Failed -> { Log.e(TAG, "Load error ", result.exception) _priorityListState.postValue(PriorityListState.LoadingFailed(shouldObserve)) } } } } fun updatePriorityList(newPriorityList: List, category: @HealthDataCategoryInt Int) { _priorityListState.postValue(PriorityListState.Loading(false)) viewModelScope.launch { updatePriorityListUseCase.invoke(newPriorityList, category) updateMostRecentAggregations(category) val appMetadataList: List = newPriorityList.map { appInfoReader.getAppMetadata(it) } _priorityListState.postValue(PriorityListState.WithData(false, appMetadataList)) } } private fun updateMostRecentAggregations(category: @HealthDataCategoryInt Int) { _aggregationCardsData.postValue(AggregationCardsState.Loading(false)) _updatedAggregationCardsData.postValue(AggregationCardsState.Loading(true)) viewModelScope.launch { val job = async { loadDatesWithDataUseCase.invoke(category) } delay(1000) when (val aggregationInfoResult = job.await()) { is UseCaseResults.Success -> { _aggregationCardsData.postValue( AggregationCardsState.WithData(false, aggregationInfoResult.data) ) _updatedAggregationCardsData.postValue( AggregationCardsState.WithData(true, aggregationInfoResult.data) ) } is UseCaseResults.Failed -> { Log.e(TAG, "Failed loading dates with data ", aggregationInfoResult.exception) _aggregationCardsData.postValue(AggregationCardsState.LoadingFailed(false)) _updatedAggregationCardsData.postValue( AggregationCardsState.LoadingFailed(true) ) } } } } fun getPriorityList(): List = when (val list = _priorityListState.value) { is PriorityListState.WithData -> list.priorityList else -> emptyList() } sealed class AggregationCardsState(open val shouldObserve: Boolean) { data class Loading(override val shouldObserve: Boolean) : AggregationCardsState(shouldObserve) data class LoadingFailed(override val shouldObserve: Boolean) : AggregationCardsState(shouldObserve) data class WithData( override val shouldObserve: Boolean, val dataTotals: List, ) : AggregationCardsState(shouldObserve) } sealed class PotentialAppSourcesState(open val shouldObserve: Boolean) { data class Loading(override val shouldObserve: Boolean) : PotentialAppSourcesState(shouldObserve) data class LoadingFailed(override val shouldObserve: Boolean) : PotentialAppSourcesState(shouldObserve) data class WithData( override val shouldObserve: Boolean, val appSources: List, ) : PotentialAppSourcesState(shouldObserve) } sealed class PriorityListState(open val shouldObserve: Boolean) { data class Loading(override val shouldObserve: Boolean) : PriorityListState(shouldObserve) data class LoadingFailed(override val shouldObserve: Boolean) : PriorityListState(shouldObserve) data class WithData( override val shouldObserve: Boolean, val priorityList: List, ) : PriorityListState(shouldObserve) } class DataSourcesInfo( val priorityListState: PriorityListState?, val potentialAppSourcesState: PotentialAppSourcesState?, ) { fun isLoading(): Boolean { return priorityListState is PriorityListState.Loading || potentialAppSourcesState is PotentialAppSourcesState.Loading } fun isLoadingFailed(): Boolean { return priorityListState is PriorityListState.LoadingFailed || potentialAppSourcesState is PotentialAppSourcesState.LoadingFailed } fun isWithData(): Boolean { return priorityListState is PriorityListState.WithData && potentialAppSourcesState is PotentialAppSourcesState.WithData } } data class DataSourcesAndAggregationsInfo( val priorityListState: PriorityListState?, val potentialAppSourcesState: PotentialAppSourcesState?, val aggregationCardsState: AggregationCardsState?, ) { fun isLoading(): Boolean { return priorityListState is PriorityListState.Loading || potentialAppSourcesState is PotentialAppSourcesState.Loading || aggregationCardsState is AggregationCardsState.Loading } fun isLoadingFailed(): Boolean { return priorityListState is PriorityListState.LoadingFailed || potentialAppSourcesState is PotentialAppSourcesState.LoadingFailed || aggregationCardsState is AggregationCardsState.LoadingFailed } fun isWithData(): Boolean { return priorityListState is PriorityListState.WithData && potentialAppSourcesState is PotentialAppSourcesState.WithData && aggregationCardsState is AggregationCardsState.WithData } } }