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