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