1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  *
17  */
18 
19 package com.android.healthconnect.controller.permissions.additionalaccess
20 
21 import android.health.connect.HealthPermissions
22 import android.health.connect.HealthPermissions.READ_EXERCISE
23 import android.health.connect.HealthPermissions.READ_EXERCISE_ROUTES
24 import androidx.lifecycle.LiveData
25 import androidx.lifecycle.MediatorLiveData
26 import androidx.lifecycle.MutableLiveData
27 import androidx.lifecycle.ViewModel
28 import androidx.lifecycle.viewModelScope
29 import com.android.healthconnect.controller.permissions.additionalaccess.PermissionUiState.ALWAYS_ALLOW
30 import com.android.healthconnect.controller.permissions.additionalaccess.PermissionUiState.ASK_EVERY_TIME
31 import com.android.healthconnect.controller.permissions.additionalaccess.PermissionUiState.NEVER_ALLOW
32 import com.android.healthconnect.controller.permissions.additionalaccess.PermissionUiState.NOT_DECLARED
33 import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
34 import com.android.healthconnect.controller.permissions.api.GrantHealthPermissionUseCase
35 import com.android.healthconnect.controller.permissions.api.LoadAccessDateUseCase
36 import com.android.healthconnect.controller.permissions.api.RevokeHealthPermissionUseCase
37 import com.android.healthconnect.controller.permissions.api.SetHealthPermissionsUserFixedFlagValueUseCase
38 import com.android.healthconnect.controller.permissions.data.HealthPermission
39 import com.android.healthconnect.controller.permissions.data.PermissionsAccessType
40 import com.android.healthconnect.controller.shared.app.AppInfoReader
41 import com.android.healthconnect.controller.shared.app.AppMetadata
42 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
43 import com.android.healthconnect.controller.utils.FeatureUtils
44 import dagger.hilt.android.lifecycle.HiltViewModel
45 import javax.inject.Inject
46 import kotlinx.coroutines.launch
47 
48 /** View model for [AdditionalAccessFragment]. */
49 @HiltViewModel
50 class AdditionalAccessViewModel
51 @Inject
52 constructor(
53     private val featureUtils: FeatureUtils,
54     private val appInfoReader: AppInfoReader,
55     private val loadExerciseRoutePermissionUseCase: LoadExerciseRoutePermissionUseCase,
56     private val grantHealthPermissionUseCase: GrantHealthPermissionUseCase,
57     private val revokeHealthPermissionUseCase: RevokeHealthPermissionUseCase,
58     private val setHealthPermissionsUserFixedFlagValueUseCase:
59         SetHealthPermissionsUserFixedFlagValueUseCase,
60     private val getAdditionalPermissionUseCase: GetAdditionalPermissionUseCase,
61     private val getGrantedHealthPermissionsUseCase: GetGrantedHealthPermissionsUseCase,
62     private val loadAccessDateUseCase: LoadAccessDateUseCase
63 ) : ViewModel() {
64 
65     private val _additionalAccessState = MutableLiveData<State>()
66     val additionalAccessState: LiveData<State>
67         get() = _additionalAccessState
68 
69     private val _showEnableExerciseEvent = MutableLiveData(false)
70     private val _appInfo = MutableLiveData<AppMetadata>()
71 
72     val showEnableExerciseEvent =
73         MediatorLiveData(EnableExerciseDialogEvent()).apply {
74             addSource(_showEnableExerciseEvent) {
75                 postValue(
76                     EnableExerciseDialogEvent(
77                         shouldShowDialog = _showEnableExerciseEvent.value ?: false,
78                         appName = _appInfo.value?.appName ?: ""))
79             }
80             addSource(_appInfo) {
81                 postValue(
82                     EnableExerciseDialogEvent(
83                         shouldShowDialog = _showEnableExerciseEvent.value ?: false,
84                         appName = _appInfo.value?.appName ?: ""))
85             }
86         }
87 
88     fun loadAccessDate(packageName: String) = loadAccessDateUseCase.invoke(packageName)
89 
90     /** Loads available additional access preferences. */
91     fun loadAdditionalAccessPreferences(packageName: String) {
92         viewModelScope.launch {
93             _appInfo.postValue(appInfoReader.getAppMetadata(packageName))
94             var newState = State()
95             if (featureUtils.isExerciseRouteReadAllEnabled()) {
96                 newState =
97                     when (val result = loadExerciseRoutePermissionUseCase(packageName)) {
98                         is UseCaseResults.Success -> {
99                             newState.copy(
100                                 exerciseRoutePermissionUIState =
101                                     result.data.exerciseRoutePermissionState,
102                                 exercisePermissionUIState = result.data.exercisePermissionState)
103                         }
104                         else -> {
105                             newState.copy(
106                                 exerciseRoutePermissionUIState = NOT_DECLARED,
107                                 exercisePermissionUIState = NOT_DECLARED)
108                         }
109                     }
110             }
111 
112             val additionalPermissions = getAdditionalPermissionUseCase(packageName)
113             val grantedPermissions = getGrantedHealthPermissionsUseCase.invoke(packageName)
114             val isAnyReadPermissionGranted =
115                 grantedPermissions.any { permission -> isDataTypeReadPermission(permission) }
116             if (featureUtils.isBackgroundReadEnabled() &&
117                 additionalPermissions.contains(HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND)) {
118                 newState =
119                     newState.copy(
120                         backgroundReadUIState =
121                             AdditionalPermissionState(
122                                 isDeclared = true,
123                                 isGranted =
124                                     grantedPermissions.contains(
125                                         HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND),
126                                 isEnabled = isAnyReadPermissionGranted))
127             }
128             if (featureUtils.isHistoryReadEnabled() &&
129                 additionalPermissions.contains(HealthPermissions.READ_HEALTH_DATA_HISTORY)) {
130                 newState =
131                     newState.copy(
132                         historyReadUIState =
133                             AdditionalPermissionState(
134                                 isDeclared = true,
135                                 isGranted =
136                                     grantedPermissions.contains(
137                                         HealthPermissions.READ_HEALTH_DATA_HISTORY),
138                                 isEnabled = isAnyReadPermissionGranted))
139             }
140 
141             _additionalAccessState.postValue(newState)
142         }
143     }
144 
145     private fun isDataTypeReadPermission(permission: String): Boolean {
146         val healthPermission = HealthPermission.fromPermissionString(permission)
147         return ((healthPermission is HealthPermission.FitnessPermission) &&
148             healthPermission.permissionsAccessType == PermissionsAccessType.READ)
149     }
150 
151     /** Updates exercise route permission state and refreshes the screen state. */
152     fun updateExerciseRouteState(packageName: String, exerciseRouteNewState: PermissionUiState) {
153         val screenState = _additionalAccessState.value
154         if (screenState == null ||
155             screenState.exerciseRoutePermissionUIState == exerciseRouteNewState)
156             return
157         when (exerciseRouteNewState) {
158             ALWAYS_ALLOW -> {
159                 // apps who are granted [READ_EXERCISE_ROUTES] should also be granted
160                 // [READ_EXERCISE]
161                 if (canEnableExercisePermission(screenState)) {
162                     _showEnableExerciseEvent.postValue(true)
163                 } else {
164                     grantHealthPermissionUseCase(packageName, READ_EXERCISE_ROUTES)
165                 }
166             }
167             ASK_EVERY_TIME -> {
168                 if (screenState.exerciseRoutePermissionUIState == ALWAYS_ALLOW) {
169                     revokeHealthPermissionUseCase(packageName, READ_EXERCISE_ROUTES)
170                 } else if (screenState.exerciseRoutePermissionUIState == NEVER_ALLOW) {
171                     setHealthPermissionsUserFixedFlagValueUseCase(
172                         packageName, listOf(READ_EXERCISE_ROUTES), false)
173                 }
174             }
175             else -> {
176                 if (screenState.exerciseRoutePermissionUIState == ALWAYS_ALLOW) {
177                     revokeHealthPermissionUseCase(packageName, READ_EXERCISE_ROUTES)
178                 }
179                 setHealthPermissionsUserFixedFlagValueUseCase(
180                     packageName, listOf(READ_EXERCISE_ROUTES), true)
181             }
182         }
183         // refresh the ui
184         loadAdditionalAccessPreferences(packageName)
185     }
186 
187     private fun canEnableExercisePermission(screenState: State): Boolean {
188         return screenState.exercisePermissionUIState == ASK_EVERY_TIME ||
189             screenState.exercisePermissionUIState == NEVER_ALLOW
190     }
191 
192     fun enableExercisePermission(packageName: String) {
193         grantHealthPermissionUseCase(packageName, READ_EXERCISE_ROUTES)
194         grantHealthPermissionUseCase(packageName, READ_EXERCISE)
195     }
196 
197     fun hideExercisePermissionRequestDialog() {
198         _showEnableExerciseEvent.postValue(false)
199     }
200 
201     fun updatePermission(packageName: String, permission: String, grant: Boolean) {
202         if (grant) {
203             grantHealthPermissionUseCase.invoke(packageName, permission)
204         } else {
205             revokeHealthPermissionUseCase.invoke(packageName, permission)
206         }
207     }
208 
209     /** Holds [AdditionalAccessFragment] UI state. */
210     data class State(
211         val exerciseRoutePermissionUIState: PermissionUiState = NOT_DECLARED,
212         val exercisePermissionUIState: PermissionUiState = NOT_DECLARED,
213         val backgroundReadUIState: AdditionalPermissionState = AdditionalPermissionState(),
214         val historyReadUIState: AdditionalPermissionState = AdditionalPermissionState()
215     ) {
216 
217         /**
218          * Checks if Additional access screen state is valid and will have options to show in the
219          * screen.
220          *
221          * Used by [SettingsManageAppPermissionsFragment] to decide to show additional access entry
222          * point.
223          */
224         fun isValid(): Boolean {
225             return (exerciseRoutePermissionUIState != NOT_DECLARED &&
226                 exercisePermissionUIState != NOT_DECLARED) ||
227                 backgroundReadUIState.isDeclared ||
228                 historyReadUIState.isDeclared
229         }
230 
231         fun showFooter(): Boolean {
232             return isAdditionalPermissionDisabled(backgroundReadUIState) ||
233                 isAdditionalPermissionDisabled(historyReadUIState)
234         }
235 
236         fun isAdditionalPermissionDisabled(
237             additionalPermissionState: AdditionalPermissionState
238         ): Boolean {
239             return additionalPermissionState.isDeclared && !additionalPermissionState.isEnabled
240         }
241     }
242 
243     data class AdditionalPermissionState(
244         val isDeclared: Boolean = false,
245         val isEnabled: Boolean = false,
246         val isGranted: Boolean = false
247     )
248 
249     data class EnableExerciseDialogEvent(
250         val shouldShowDialog: Boolean = false,
251         val appName: String = ""
252     )
253 }
254