• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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