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 _editedPotentialAppSources = MutableLiveData<List<AppMetadata>>()
65 
66     private val _currentPriorityList = MutableLiveData<PriorityListState>()
67 
68     private val _editedPriorityList = MutableLiveData<List<AppMetadata>>()
69 
70     private val _dataSourcesAndAggregationsInfo = MediatorLiveData<DataSourcesAndAggregationsInfo>()
71     val dataSourcesAndAggregationsInfo: LiveData<DataSourcesAndAggregationsInfo>
72         get() = _dataSourcesAndAggregationsInfo
73 
74     private val _dataSourcesInfo = MediatorLiveData<DataSourcesInfo>()
75     val dataSourcesInfo: LiveData<DataSourcesInfo>
76         get() = _dataSourcesInfo
77 
78     init {
79         _dataSourcesAndAggregationsInfo.addSource(_currentPriorityList) { priorityListState ->
80             if (!priorityListState.shouldObserve) {
81                 return@addSource
82             }
83             _dataSourcesAndAggregationsInfo.value =
84                 DataSourcesAndAggregationsInfo(
85                     priorityListState = priorityListState,
86                     potentialAppSourcesState = _potentialAppSources.value,
87                     aggregationCardsState = _aggregationCardsData.value)
88         }
89         _dataSourcesAndAggregationsInfo.addSource(_potentialAppSources) { potentialAppSourcesState
90             ->
91             if (!potentialAppSourcesState.shouldObserve) {
92                 return@addSource
93             }
94             _dataSourcesAndAggregationsInfo.value =
95                 DataSourcesAndAggregationsInfo(
96                     priorityListState = _currentPriorityList.value,
97                     potentialAppSourcesState = potentialAppSourcesState,
98                     aggregationCardsState = _aggregationCardsData.value)
99         }
100         _dataSourcesAndAggregationsInfo.addSource(_aggregationCardsData) { aggregationCardsState ->
101             if (!aggregationCardsState.shouldObserve) {
102                 return@addSource
103             }
104             _dataSourcesAndAggregationsInfo.value =
105                 DataSourcesAndAggregationsInfo(
106                     priorityListState = _currentPriorityList.value,
107                     potentialAppSourcesState = _potentialAppSources.value,
108                     aggregationCardsState = aggregationCardsState)
109         }
110 
111         _dataSourcesInfo.addSource(_currentPriorityList) { priorityListState ->
112             _dataSourcesInfo.value =
113                 DataSourcesInfo(
114                     priorityListState = priorityListState,
115                     potentialAppSourcesState = _potentialAppSources.value)
116         }
117 
118         _dataSourcesInfo.addSource(_potentialAppSources) { potentialAppSourcesState ->
119             _dataSourcesInfo.value =
120                 DataSourcesInfo(
121                     priorityListState = _currentPriorityList.value,
122                     potentialAppSourcesState = potentialAppSourcesState)
123         }
124     }
125 
126     private var currentSelection = HealthDataCategory.ACTIVITY
127 
128     fun getCurrentSelection(): Int = currentSelection
129 
130     fun setCurrentSelection(category: @HealthDataCategoryInt Int) {
131         currentSelection = category
132     }
133 
134     fun loadData(category: @HealthDataCategoryInt Int) {
135         loadMostRecentAggregations(category)
136         loadCurrentPriorityList(category)
137         loadPotentialAppSources(category)
138     }
139 
140     private fun loadMostRecentAggregations(category: @HealthDataCategoryInt Int) {
141         _aggregationCardsData.postValue(AggregationCardsState.Loading(true))
142         viewModelScope.launch {
143             when (val aggregationInfoResult = loadDatesWithDataUseCase.invoke(category)) {
144                 is UseCaseResults.Success -> {
145                     _aggregationCardsData.postValue(
146                         AggregationCardsState.WithData(true, aggregationInfoResult.data))
147                 }
148                 is UseCaseResults.Failed -> {
149                     Log.e(TAG, "Failed loading dates with data ", aggregationInfoResult.exception)
150                     _aggregationCardsData.postValue(AggregationCardsState.LoadingFailed(true))
151                 }
152             }
153         }
154     }
155 
156     fun loadPotentialAppSources(
157         category: @HealthDataCategoryInt Int,
158         shouldObserve: Boolean = true
159     ) {
160         _potentialAppSources.postValue(PotentialAppSourcesState.Loading(shouldObserve))
161         viewModelScope.launch {
162             when (val appSourcesResult = loadPotentialAppSourcesUseCase.invoke(category)) {
163                 is UseCaseResults.Success -> {
164                     _potentialAppSources.postValue(
165                         PotentialAppSourcesState.WithData(shouldObserve, appSourcesResult.data))
166                 }
167                 is UseCaseResults.Failed -> {
168                     Log.e(
169                         TAG,
170                         "Failed to load possible priority list candidates",
171                         appSourcesResult.exception)
172                     _potentialAppSources.postValue(
173                         PotentialAppSourcesState.LoadingFailed(shouldObserve))
174                 }
175             }
176         }
177     }
178 
179     private fun loadCurrentPriorityList(category: @HealthDataCategoryInt Int) {
180         _currentPriorityList.postValue(PriorityListState.Loading(true))
181         viewModelScope.launch {
182             when (val result = loadPriorityListUseCase.invoke(category)) {
183                 is UseCaseResults.Success ->
184                     _currentPriorityList.postValue(
185                         if (result.data.isEmpty()) {
186                             PriorityListState.WithData(true, listOf())
187                         } else {
188                             PriorityListState.WithData(true, result.data)
189                         })
190                 is UseCaseResults.Failed -> {
191                     Log.e(TAG, "Load error ", result.exception)
192                     _currentPriorityList.postValue(PriorityListState.LoadingFailed(true))
193                 }
194             }
195         }
196     }
197 
198     fun updatePriorityList(newPriorityList: List<String>, category: @HealthDataCategoryInt Int) {
199         _currentPriorityList.postValue(PriorityListState.Loading(false))
200         viewModelScope.launch {
201             updatePriorityListUseCase.invoke(newPriorityList, category)
202             updateMostRecentAggregations(category)
203             val appMetadataList: List<AppMetadata> =
204                 newPriorityList.map { appInfoReader.getAppMetadata(it) }
205             _currentPriorityList.postValue(PriorityListState.WithData(false, appMetadataList))
206         }
207     }
208 
209     private fun updateMostRecentAggregations(category: @HealthDataCategoryInt Int) {
210         _aggregationCardsData.postValue(AggregationCardsState.Loading(false))
211         _updatedAggregationCardsData.postValue(AggregationCardsState.Loading(true))
212         viewModelScope.launch {
213             val job = async { loadDatesWithDataUseCase.invoke(category) }
214             delay(1000)
215 
216             when (val aggregationInfoResult = job.await()) {
217                 is UseCaseResults.Success -> {
218                     _aggregationCardsData.postValue(
219                         AggregationCardsState.WithData(false, aggregationInfoResult.data))
220                     _updatedAggregationCardsData.postValue(
221                         AggregationCardsState.WithData(true, aggregationInfoResult.data))
222                 }
223                 is UseCaseResults.Failed -> {
224                     Log.e(TAG, "Failed loading dates with data ", aggregationInfoResult.exception)
225                     _aggregationCardsData.postValue(AggregationCardsState.LoadingFailed(false))
226                     _updatedAggregationCardsData.postValue(
227                         AggregationCardsState.LoadingFailed(true))
228                 }
229             }
230         }
231     }
232 
233     fun setEditedPriorityList(newList: List<AppMetadata>) {
234         _editedPriorityList.value = newList
235     }
236 
237     fun setEditedPotentialAppSources(newList: List<AppMetadata>) {
238         _editedPotentialAppSources.value = newList
239     }
240 
241     fun getEditedPotentialAppSources(): List<AppMetadata> {
242         return _editedPotentialAppSources.value ?: emptyList()
243     }
244 
245     fun getEditedPriorityList(): List<AppMetadata> {
246         return _editedPriorityList.value ?: emptyList()
247     }
248 
249     sealed class AggregationCardsState(open val shouldObserve: Boolean) {
250         data class Loading(override val shouldObserve: Boolean) :
251             AggregationCardsState(shouldObserve)
252 
253         data class LoadingFailed(override val shouldObserve: Boolean) :
254             AggregationCardsState(shouldObserve)
255 
256         data class WithData(
257             override val shouldObserve: Boolean,
258             val dataTotals: List<AggregationCardInfo>
259         ) : AggregationCardsState(shouldObserve)
260     }
261 
262     sealed class PotentialAppSourcesState(open val shouldObserve: Boolean) {
263         data class Loading(override val shouldObserve: Boolean) :
264             PotentialAppSourcesState(shouldObserve)
265 
266         data class LoadingFailed(override val shouldObserve: Boolean) :
267             PotentialAppSourcesState(shouldObserve)
268 
269         data class WithData(
270             override val shouldObserve: Boolean,
271             val appSources: List<AppMetadata>
272         ) : PotentialAppSourcesState(shouldObserve)
273     }
274 
275     sealed class PriorityListState(open val shouldObserve: Boolean) {
276         data class Loading(override val shouldObserve: Boolean) : PriorityListState(shouldObserve)
277 
278         data class LoadingFailed(override val shouldObserve: Boolean) :
279             PriorityListState(shouldObserve)
280 
281         data class WithData(
282             override val shouldObserve: Boolean,
283             val priorityList: List<AppMetadata>
284         ) : PriorityListState(shouldObserve)
285     }
286 
287     class DataSourcesInfo(
288         val priorityListState: PriorityListState?,
289         val potentialAppSourcesState: PotentialAppSourcesState?
290     ) {
291         fun isLoading(): Boolean {
292             return priorityListState is PriorityListState.Loading ||
293                 potentialAppSourcesState is PotentialAppSourcesState.Loading
294         }
295 
296         fun isLoadingFailed(): Boolean {
297             return priorityListState is PriorityListState.LoadingFailed ||
298                 potentialAppSourcesState is PotentialAppSourcesState.LoadingFailed
299         }
300 
301         fun isWithData(): Boolean {
302             return priorityListState is PriorityListState.WithData &&
303                 potentialAppSourcesState is PotentialAppSourcesState.WithData
304         }
305     }
306 
307     data class DataSourcesAndAggregationsInfo(
308         val priorityListState: PriorityListState?,
309         val potentialAppSourcesState: PotentialAppSourcesState?,
310         val aggregationCardsState: AggregationCardsState?
311     ) {
312         fun isLoading(): Boolean {
313             return priorityListState is PriorityListState.Loading ||
314                 potentialAppSourcesState is PotentialAppSourcesState.Loading ||
315                 aggregationCardsState is AggregationCardsState.Loading
316         }
317 
318         fun isLoadingFailed(): Boolean {
319             return priorityListState is PriorityListState.LoadingFailed ||
320                 potentialAppSourcesState is PotentialAppSourcesState.LoadingFailed ||
321                 aggregationCardsState is AggregationCardsState.LoadingFailed
322         }
323 
324         fun isWithData(): Boolean {
325             return priorityListState is PriorityListState.WithData &&
326                 potentialAppSourcesState is PotentialAppSourcesState.WithData &&
327                 aggregationCardsState is AggregationCardsState.WithData
328         }
329     }
330 }
331