1 /** <lambda>null2 * 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 * 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.permissions.app 15 16 import android.health.connect.HealthPermissions.READ_EXERCISE 17 import android.health.connect.HealthPermissions.READ_EXERCISE_ROUTES 18 import android.health.connect.TimeInstantRangeFilter 19 import android.util.Log 20 import androidx.annotation.VisibleForTesting 21 import androidx.lifecycle.LiveData 22 import androidx.lifecycle.MediatorLiveData 23 import androidx.lifecycle.MutableLiveData 24 import androidx.lifecycle.ViewModel 25 import androidx.lifecycle.viewModelScope 26 import com.android.healthconnect.controller.deletion.DeletionType.DeletionTypeAppData 27 import com.android.healthconnect.controller.deletion.api.DeleteAppDataUseCase as OldDeleteAppDataUseCase 28 import com.android.healthconnect.controller.permissions.additionalaccess.ILoadExerciseRoutePermissionUseCase 29 import com.android.healthconnect.controller.permissions.additionalaccess.PermissionUiState.ALWAYS_ALLOW 30 import com.android.healthconnect.controller.permissions.api.GrantHealthPermissionUseCase 31 import com.android.healthconnect.controller.permissions.api.IGetGrantedHealthPermissionsUseCase 32 import com.android.healthconnect.controller.permissions.api.LoadAccessDateUseCase 33 import com.android.healthconnect.controller.permissions.api.RevokeAllHealthPermissionsUseCase 34 import com.android.healthconnect.controller.permissions.api.RevokeHealthPermissionUseCase 35 import com.android.healthconnect.controller.permissions.data.HealthPermission.AdditionalPermission 36 import com.android.healthconnect.controller.permissions.data.HealthPermission.FitnessPermission 37 import com.android.healthconnect.controller.permissions.data.HealthPermission.FitnessPermission.Companion.fromPermissionString 38 import com.android.healthconnect.controller.permissions.data.HealthPermission.MedicalPermission 39 import com.android.healthconnect.controller.permissions.data.MedicalPermissionType 40 import com.android.healthconnect.controller.permissions.data.PermissionsAccessType 41 import com.android.healthconnect.controller.selectabledeletion.DeletionType.DeleteAppData 42 import com.android.healthconnect.controller.selectabledeletion.api.DeleteAppDataUseCase 43 import com.android.healthconnect.controller.service.IoDispatcher 44 import com.android.healthconnect.controller.shared.HealthPermissionReader 45 import com.android.healthconnect.controller.shared.app.AppInfoReader 46 import com.android.healthconnect.controller.shared.app.AppMetadata 47 import com.android.healthconnect.controller.shared.usecase.UseCaseResults 48 import com.android.healthfitness.flags.Flags.newInformationArchitecture 49 import com.android.healthfitness.flags.Flags.personalHealthRecord 50 import dagger.hilt.android.lifecycle.HiltViewModel 51 import java.time.Instant 52 import javax.inject.Inject 53 import kotlinx.coroutines.CoroutineDispatcher 54 import kotlinx.coroutines.launch 55 import kotlinx.coroutines.runBlocking 56 57 /** View model for {@link FitnessAppFragment} and {SettingsManageAppPermissionsFragment} . */ 58 @HiltViewModel 59 class AppPermissionViewModel 60 @Inject 61 constructor( 62 private val appInfoReader: AppInfoReader, 63 private val loadAppPermissionsStatusUseCase: LoadAppPermissionsStatusUseCase, 64 private val grantPermissionsStatusUseCase: GrantHealthPermissionUseCase, 65 private val revokePermissionsStatusUseCase: RevokeHealthPermissionUseCase, 66 private val revokeAllHealthPermissionsUseCase: RevokeAllHealthPermissionsUseCase, 67 private val deleteAppDataUseCase: DeleteAppDataUseCase, 68 private val oldDeleteAppDataUseCase: OldDeleteAppDataUseCase, 69 private val loadAccessDateUseCase: LoadAccessDateUseCase, 70 private val loadGrantedHealthPermissionsUseCase: IGetGrantedHealthPermissionsUseCase, 71 private val loadExerciseRoutePermissionUseCase: ILoadExerciseRoutePermissionUseCase, 72 private val healthPermissionReader: HealthPermissionReader, 73 @IoDispatcher private val ioDispatcher: CoroutineDispatcher, 74 ) : ViewModel() { 75 76 companion object { 77 private const val TAG = "AppPermissionViewModel" 78 } 79 80 private val _fitnessPermissions = MutableLiveData<List<FitnessPermission>>(emptyList()) 81 val fitnessPermissions: LiveData<List<FitnessPermission>> 82 get() = _fitnessPermissions 83 84 private val _grantedFitnessPermissions = MutableLiveData<Set<FitnessPermission>>(emptySet()) 85 val grantedFitnessPermissions: LiveData<Set<FitnessPermission>> 86 get() = _grantedFitnessPermissions 87 88 val allFitnessPermissionsGranted = 89 MediatorLiveData(false).apply { 90 addSource(_fitnessPermissions) { 91 postValue( 92 isAllFitnessPermissionsGranted(fitnessPermissions, grantedFitnessPermissions) 93 ) 94 } 95 addSource(_grantedFitnessPermissions) { 96 postValue( 97 isAllFitnessPermissionsGranted(fitnessPermissions, grantedFitnessPermissions) 98 ) 99 } 100 } 101 102 val atLeastOneFitnessPermissionGranted = 103 MediatorLiveData(false).apply { 104 addSource(_grantedFitnessPermissions) { grantedPermissions -> 105 postValue(grantedPermissions.isNotEmpty()) 106 } 107 } 108 109 private val _medicalPermissions = MutableLiveData<List<MedicalPermission>>(emptyList()) 110 val medicalPermissions: LiveData<List<MedicalPermission>> 111 get() = _medicalPermissions 112 113 private val _grantedMedicalPermissions = MutableLiveData<Set<MedicalPermission>>(emptySet()) 114 val grantedMedicalPermissions: LiveData<Set<MedicalPermission>> 115 get() = _grantedMedicalPermissions 116 117 private var _additionalPermissions = MutableLiveData<List<AdditionalPermission>>(emptyList()) 118 private var _grantedAdditionalPermissions = 119 MutableLiveData<Set<AdditionalPermission>>(emptySet()) 120 @VisibleForTesting 121 val grantedAdditionalPermissions: LiveData<Set<AdditionalPermission>> 122 get() = _grantedAdditionalPermissions 123 124 val allMedicalPermissionsGranted = 125 MediatorLiveData(false).apply { 126 addSource(_medicalPermissions) { 127 postValue( 128 isAllMedicalPermissionsGranted(medicalPermissions, grantedMedicalPermissions) 129 ) 130 } 131 addSource(_grantedMedicalPermissions) { 132 postValue( 133 isAllMedicalPermissionsGranted(medicalPermissions, grantedMedicalPermissions) 134 ) 135 } 136 } 137 138 val atLeastOneMedicalPermissionGranted = 139 MediatorLiveData(false).apply { 140 addSource(_grantedMedicalPermissions) { grantedPermissions -> 141 postValue(grantedPermissions.isNotEmpty()) 142 } 143 } 144 145 val atLeastOneHealthPermissionGranted = 146 MediatorLiveData(false).apply { 147 addSource(atLeastOneFitnessPermissionGranted) { value -> 148 this.value = value || atLeastOneMedicalPermissionGranted.value ?: false 149 } 150 addSource(atLeastOneMedicalPermissionGranted) { value -> 151 this.value = value || atLeastOneFitnessPermissionGranted.value ?: false 152 } 153 } 154 155 private fun atLeastOneMedicalReadPermissionGranted(): Boolean = 156 _grantedMedicalPermissions.value 157 .orEmpty() 158 .filterNot { perm -> 159 perm.medicalPermissionType == MedicalPermissionType.ALL_MEDICAL_DATA 160 } 161 .isNotEmpty() 162 163 private fun atLeastOneFitnessReadPermissionGranted(): Boolean = 164 _grantedFitnessPermissions.value.orEmpty().any { perm -> 165 perm.permissionsAccessType == PermissionsAccessType.READ 166 } 167 168 fun revokeFitnessShouldIncludeBackground(): Boolean = 169 _additionalPermissions.value 170 .orEmpty() 171 .contains(AdditionalPermission.READ_HEALTH_DATA_IN_BACKGROUND) && 172 atLeastOneFitnessReadPermissionGranted() && 173 !atLeastOneMedicalReadPermissionGranted() 174 175 fun revokeFitnessShouldIncludePastData(): Boolean = 176 _additionalPermissions.value 177 .orEmpty() 178 .contains(AdditionalPermission.READ_HEALTH_DATA_HISTORY) && 179 atLeastOneFitnessReadPermissionGranted() && 180 !atLeastOneMedicalReadPermissionGranted() 181 182 fun revokeMedicalShouldIncludeBackground(): Boolean = 183 _additionalPermissions.value 184 .orEmpty() 185 .contains(AdditionalPermission.READ_HEALTH_DATA_IN_BACKGROUND) && 186 atLeastOneMedicalReadPermissionGranted() && 187 !atLeastOneFitnessReadPermissionGranted() 188 189 fun revokeMedicalShouldIncludePastData(): Boolean = 190 _additionalPermissions.value 191 .orEmpty() 192 .contains(AdditionalPermission.READ_HEALTH_DATA_HISTORY) && 193 atLeastOneMedicalReadPermissionGranted() && 194 !atLeastOneFitnessReadPermissionGranted() 195 196 fun revokeAllShouldIncludeBackground(): Boolean = 197 _additionalPermissions.value 198 .orEmpty() 199 .contains(AdditionalPermission.READ_HEALTH_DATA_IN_BACKGROUND) 200 201 fun revokeAllShouldIncludePastData(): Boolean = 202 _additionalPermissions.value 203 .orEmpty() 204 .contains(AdditionalPermission.READ_HEALTH_DATA_HISTORY) 205 206 private val _appInfo = MutableLiveData<AppMetadata>() 207 val appInfo: LiveData<AppMetadata> 208 get() = _appInfo 209 210 private val _revokeAllHealthPermissionsState = 211 MutableLiveData<RevokeAllState>(RevokeAllState.NotStarted) 212 val revokeAllHealthPermissionsState: LiveData<RevokeAllState> 213 get() = _revokeAllHealthPermissionsState 214 215 private var healthPermissionsList: List<HealthPermissionStatus> = listOf() 216 217 /** 218 * Flag to prevent {@link SettingManageAppPermissionsFragment} from reloading the granted 219 * permissions on orientation change 220 */ 221 private var shouldLoadGrantedPermissions = true 222 223 private val _showDisableExerciseRouteEvent = MutableLiveData(false) 224 val showDisableExerciseRouteEvent = 225 MediatorLiveData(DisableExerciseRouteDialogEvent()).apply { 226 addSource(_showDisableExerciseRouteEvent) { 227 postValue( 228 DisableExerciseRouteDialogEvent( 229 shouldShowDialog = _showDisableExerciseRouteEvent.value ?: false, 230 appName = _appInfo.value?.appName ?: "", 231 ) 232 ) 233 } 234 addSource(_appInfo) { 235 postValue( 236 DisableExerciseRouteDialogEvent( 237 shouldShowDialog = _showDisableExerciseRouteEvent.value ?: false, 238 appName = _appInfo.value?.appName ?: "", 239 ) 240 ) 241 } 242 } 243 244 private val _lastReadPermissionDisconnected = MutableLiveData(false) 245 val lastReadPermissionDisconnected: LiveData<Boolean> 246 get() = _lastReadPermissionDisconnected 247 248 private val newDeletionFlow = personalHealthRecord() || newInformationArchitecture() 249 250 fun loadPermissionsForPackage(packageName: String) { 251 // clear app permissions 252 _fitnessPermissions.postValue(emptyList()) 253 _grantedFitnessPermissions.postValue(emptySet()) 254 _medicalPermissions.postValue(emptyList()) 255 _grantedMedicalPermissions.postValue(emptySet()) 256 _additionalPermissions.postValue(emptyList()) 257 _grantedAdditionalPermissions.postValue(emptySet()) 258 259 viewModelScope.launch { _appInfo.postValue(appInfoReader.getAppMetadata(packageName)) } 260 if (isPackageSupported(packageName)) { 261 loadAllPermissions(packageName) 262 } else { 263 // we only load granted permissions for not supported apps to allow users to revoke 264 // these permissions. 265 loadGrantedPermissionsForPackage(packageName) 266 } 267 } 268 269 private fun loadAllPermissions(packageName: String) { 270 viewModelScope.launch { 271 healthPermissionsList = loadAppPermissionsStatusUseCase.invoke(packageName) 272 _fitnessPermissions.postValue( 273 healthPermissionsList 274 .map { it.healthPermission } 275 .filterIsInstance<FitnessPermission>() 276 ) 277 _grantedFitnessPermissions.postValue( 278 healthPermissionsList 279 .filter { it.isGranted } 280 .map { it.healthPermission } 281 .filterIsInstance<FitnessPermission>() 282 .toSet() 283 ) 284 _medicalPermissions.postValue( 285 healthPermissionsList 286 .map { it.healthPermission } 287 .filterIsInstance<MedicalPermission>() 288 ) 289 _grantedMedicalPermissions.postValue( 290 healthPermissionsList 291 .filter { it.isGranted } 292 .map { it.healthPermission } 293 .filterIsInstance<MedicalPermission>() 294 .toSet() 295 ) 296 // invalid additional permissions filtered in the useCase 297 _additionalPermissions.postValue( 298 healthPermissionsList 299 .map { it.healthPermission } 300 .filterIsInstance<AdditionalPermission>() 301 ) 302 _grantedAdditionalPermissions.postValue( 303 healthPermissionsList 304 .filter { it.isGranted } 305 .map { it.healthPermission } 306 .filterIsInstance<AdditionalPermission>() 307 .toSet() 308 ) 309 } 310 } 311 312 private fun loadGrantedPermissionsForPackage(packageName: String) { 313 // Only reload the status the first time this method is called 314 if (shouldLoadGrantedPermissions) { 315 viewModelScope.launch { 316 val grantedPermissions = 317 loadAppPermissionsStatusUseCase.invoke(packageName).filter { it.isGranted } 318 healthPermissionsList = grantedPermissions 319 320 // Only show app permissions that are granted 321 _fitnessPermissions.postValue( 322 grantedPermissions 323 .map { it.healthPermission } 324 .filterIsInstance<FitnessPermission>() 325 ) 326 _grantedFitnessPermissions.postValue( 327 grantedPermissions 328 .map { it.healthPermission } 329 .filterIsInstance<FitnessPermission>() 330 .toSet() 331 ) 332 _medicalPermissions.postValue( 333 grantedPermissions 334 .map { it.healthPermission } 335 .filterIsInstance<MedicalPermission>() 336 ) 337 _grantedMedicalPermissions.postValue( 338 grantedPermissions 339 .map { it.healthPermission } 340 .filterIsInstance<MedicalPermission>() 341 .toSet() 342 ) 343 344 _grantedAdditionalPermissions.postValue( 345 grantedPermissions 346 .map { it.healthPermission } 347 .filterIsInstance<AdditionalPermission>() 348 .toSet() 349 ) 350 } 351 shouldLoadGrantedPermissions = false 352 } 353 } 354 355 fun loadAccessDate(packageName: String): Instant? { 356 return loadAccessDateUseCase.invoke(packageName) 357 } 358 359 fun updatePermission( 360 packageName: String, 361 fitnessPermission: FitnessPermission, 362 grant: Boolean, 363 ): Boolean { 364 try { 365 if (grant) { 366 grantPermission(packageName, fitnessPermission) 367 } else { 368 if (shouldDisplayExerciseRouteDialog(packageName, fitnessPermission)) { 369 _showDisableExerciseRouteEvent.postValue(true) 370 } else { 371 revokeFitnessPermission(fitnessPermission, packageName) 372 } 373 } 374 375 return true 376 } catch (ex: Exception) { 377 Log.e(TAG, "Failed to update fitness permission!", ex) 378 } 379 return false 380 } 381 382 fun updateAdditionalPermission( 383 packageName: String, 384 additionalPermission: AdditionalPermission, 385 grant: Boolean 386 ) : Boolean { 387 try { 388 val grantedPermissions = _grantedAdditionalPermissions.value.orEmpty().toMutableSet() 389 if (grant) { 390 grantPermissionsStatusUseCase.invoke(packageName,additionalPermission.toString()) 391 grantedPermissions.add(additionalPermission) 392 } else { 393 revokePermissionsStatusUseCase.invoke(packageName, additionalPermission.toString()) 394 grantedPermissions.remove(additionalPermission) 395 } 396 _grantedAdditionalPermissions.postValue(grantedPermissions) 397 return true 398 } catch (ex: Exception) { 399 Log.e(TAG, "Failed to update additional permission!", ex) 400 } 401 return false 402 } 403 404 fun updatePermission( 405 packageName: String, 406 medicalPermission: MedicalPermission, 407 grant: Boolean, 408 ): Boolean { 409 try { 410 if (grant) { 411 grantPermission(packageName, medicalPermission) 412 } else { 413 revokeMedicalPermission(medicalPermission, packageName) 414 } 415 416 return true 417 } catch (ex: Exception) { 418 Log.e(TAG, "Failed to update medical permission!", ex) 419 } 420 return false 421 } 422 423 private fun grantPermission(packageName: String, fitnessPermission: FitnessPermission) { 424 val grantedPermissions = _grantedFitnessPermissions.value.orEmpty().toMutableSet() 425 grantPermissionsStatusUseCase.invoke(packageName, fitnessPermission.toString()) 426 grantedPermissions.add(fitnessPermission) 427 _grantedFitnessPermissions.postValue(grantedPermissions) 428 } 429 430 private fun grantPermission(packageName: String, medicalPermission: MedicalPermission) { 431 val grantedPermissions = _grantedMedicalPermissions.value.orEmpty().toMutableSet() 432 grantPermissionsStatusUseCase.invoke(packageName, medicalPermission.toString()) 433 grantedPermissions.add(medicalPermission) 434 _grantedMedicalPermissions.postValue(grantedPermissions) 435 } 436 437 private fun revokeFitnessPermission(fitnessPermission: FitnessPermission, packageName: String) { 438 val grantedFitnessPermissions = _grantedFitnessPermissions.value.orEmpty().toMutableSet() 439 val grantedMedicalPermissions = _grantedMedicalPermissions.value.orEmpty() 440 441 val readPermissionsBeforeDisconnect = 442 grantedFitnessPermissions.count { permission -> 443 permission.permissionsAccessType == PermissionsAccessType.READ 444 } + 445 grantedMedicalPermissions.count { medicalPermission -> 446 medicalPermission.medicalPermissionType != 447 MedicalPermissionType.ALL_MEDICAL_DATA 448 } 449 grantedFitnessPermissions.remove(fitnessPermission) 450 val readPermissionsAfterDisconnect = 451 grantedFitnessPermissions.count { permission -> 452 permission.permissionsAccessType == PermissionsAccessType.READ 453 } + 454 grantedMedicalPermissions.count { medicalPermission -> 455 medicalPermission.medicalPermissionType != 456 MedicalPermissionType.ALL_MEDICAL_DATA 457 } 458 _grantedFitnessPermissions.postValue(grantedFitnessPermissions) 459 460 val lastReadPermissionRevoked = 461 _grantedAdditionalPermissions.value.orEmpty().isNotEmpty() && 462 (readPermissionsBeforeDisconnect > readPermissionsAfterDisconnect) && 463 readPermissionsAfterDisconnect == 0 464 465 if (lastReadPermissionRevoked) { 466 _grantedAdditionalPermissions.value.orEmpty().forEach { permission -> 467 revokePermissionsStatusUseCase.invoke(packageName, permission.additionalPermission) 468 } 469 } 470 471 _lastReadPermissionDisconnected.postValue(lastReadPermissionRevoked) 472 revokePermissionsStatusUseCase.invoke(packageName, fitnessPermission.toString()) 473 } 474 475 private fun revokeMedicalPermission(medicalPermission: MedicalPermission, packageName: String) { 476 val grantedMedicalPermissions = _grantedMedicalPermissions.value.orEmpty().toMutableSet() 477 val grantedFitnessPermissions = _grantedFitnessPermissions.value.orEmpty() 478 479 val readPermissionsBeforeDisconnect = 480 grantedFitnessPermissions.count { permission -> 481 permission.permissionsAccessType == PermissionsAccessType.READ 482 } + 483 grantedMedicalPermissions.count { permission -> 484 permission.medicalPermissionType != MedicalPermissionType.ALL_MEDICAL_DATA 485 } 486 grantedMedicalPermissions.remove(medicalPermission) 487 val readPermissionsAfterDisconnect = 488 grantedFitnessPermissions.count { permission -> 489 permission.permissionsAccessType == PermissionsAccessType.READ 490 } + 491 grantedMedicalPermissions.count { permission -> 492 permission.medicalPermissionType != MedicalPermissionType.ALL_MEDICAL_DATA 493 } 494 _grantedMedicalPermissions.postValue(grantedMedicalPermissions) 495 496 val lastReadPermissionRevoked = 497 _grantedAdditionalPermissions.value.orEmpty().isNotEmpty() && 498 (readPermissionsBeforeDisconnect > readPermissionsAfterDisconnect) && 499 readPermissionsAfterDisconnect == 0 500 501 if (lastReadPermissionRevoked) { 502 _grantedAdditionalPermissions.value.orEmpty().forEach { permission -> 503 revokePermissionsStatusUseCase.invoke(packageName, permission.additionalPermission) 504 } 505 } 506 507 _lastReadPermissionDisconnected.postValue(lastReadPermissionRevoked) 508 revokePermissionsStatusUseCase.invoke(packageName, medicalPermission.toString()) 509 } 510 511 fun markLastReadShown() { 512 _lastReadPermissionDisconnected.postValue(false) 513 } 514 515 private fun shouldDisplayExerciseRouteDialog( 516 packageName: String, 517 fitnessPermission: FitnessPermission, 518 ): Boolean { 519 if (fitnessPermission.toString() != READ_EXERCISE) { 520 return false 521 } 522 523 return isExerciseRoutePermissionAlwaysAllow(packageName) 524 } 525 526 fun grantAllFitnessPermissions(packageName: String): Boolean { 527 try { 528 _fitnessPermissions.value?.forEach { 529 grantPermissionsStatusUseCase.invoke(packageName, it.toString()) 530 } 531 val grantedFitnessPermissions = 532 _grantedFitnessPermissions.value.orEmpty().toMutableSet() 533 grantedFitnessPermissions.addAll(_fitnessPermissions.value.orEmpty()) 534 _grantedFitnessPermissions.postValue(grantedFitnessPermissions) 535 return true 536 } catch (ex: Exception) { 537 Log.e(TAG, "Failed to update fitness permissions!", ex) 538 } 539 return false 540 } 541 542 fun grantAllMedicalPermissions(packageName: String): Boolean { 543 try { 544 _medicalPermissions.value?.forEach { 545 grantPermissionsStatusUseCase.invoke(packageName, it.toString()) 546 } 547 val grantedMedicalPermissions = 548 _grantedMedicalPermissions.value.orEmpty().toMutableSet() 549 grantedMedicalPermissions.addAll(_medicalPermissions.value.orEmpty()) 550 _grantedMedicalPermissions.postValue(grantedMedicalPermissions) 551 return true 552 } catch (ex: Exception) { 553 Log.e(TAG, "Failed to update medical permissions!", ex) 554 } 555 return false 556 } 557 558 fun disableExerciseRoutePermission(packageName: String) { 559 revokeFitnessPermission(fromPermissionString(READ_EXERCISE), packageName) 560 // the revokePermission call will automatically revoke all additional permissions 561 // including Exercise Routes if the READ_EXERCISE permission is the last READ permission 562 if (isExerciseRoutePermissionAlwaysAllow(packageName)) { 563 revokePermissionsStatusUseCase(packageName, READ_EXERCISE_ROUTES) 564 } 565 } 566 567 private fun isExerciseRoutePermissionAlwaysAllow(packageName: String): Boolean = runBlocking { 568 when (val exerciseRouteState = loadExerciseRoutePermissionUseCase(packageName)) { 569 is UseCaseResults.Success -> { 570 exerciseRouteState.data.exerciseRoutePermissionState == ALWAYS_ALLOW 571 } 572 else -> false 573 } 574 } 575 576 fun revokeAllHealthPermissions(packageName: String): Boolean { 577 // TODO (b/325729045) if there is an error within the coroutine scope 578 // it will not be caught by this statement in tests. Consider using LiveData instead 579 try { 580 viewModelScope.launch(ioDispatcher) { 581 _revokeAllHealthPermissionsState.postValue(RevokeAllState.Loading) 582 revokeAllHealthPermissionsUseCase.invoke(packageName) 583 if (isPackageSupported(packageName)) { 584 loadPermissionsForPackage(packageName) 585 } 586 _revokeAllHealthPermissionsState.postValue(RevokeAllState.Updated) 587 _grantedFitnessPermissions.postValue(emptySet()) 588 _grantedMedicalPermissions.postValue(emptySet()) 589 _grantedAdditionalPermissions.postValue(emptySet()) 590 } 591 return true 592 } catch (ex: Exception) { 593 Log.e(TAG, "Failed to update permissions!", ex) 594 } 595 return false 596 } 597 598 fun revokeAllFitnessAndMaybeAdditionalPermissions(packageName: String): Boolean { 599 try { 600 viewModelScope.launch(ioDispatcher) { 601 _revokeAllHealthPermissionsState.postValue(RevokeAllState.Loading) 602 _fitnessPermissions.value?.forEach { 603 revokePermissionsStatusUseCase.invoke(packageName, it.toString()) 604 } 605 if (!atLeastOneMedicalReadPermissionGranted()) { 606 _grantedAdditionalPermissions.value?.forEach { 607 revokePermissionsStatusUseCase.invoke(packageName, it.additionalPermission) 608 } 609 _grantedAdditionalPermissions.postValue(emptySet()) 610 } 611 _revokeAllHealthPermissionsState.postValue(RevokeAllState.Updated) 612 _grantedFitnessPermissions.postValue(emptySet()) 613 } 614 return true 615 } catch (ex: Exception) { 616 Log.e(TAG, "Failed to revoke fitness permissions!", ex) 617 } 618 return false 619 } 620 621 fun revokeAllMedicalAndMaybeAdditionalPermissions(packageName: String): Boolean { 622 try { 623 viewModelScope.launch(ioDispatcher) { 624 _revokeAllHealthPermissionsState.postValue(RevokeAllState.Loading) 625 _medicalPermissions.value?.forEach { 626 revokePermissionsStatusUseCase.invoke(packageName, it.toString()) 627 } 628 if (!atLeastOneFitnessReadPermissionGranted()) { 629 _grantedAdditionalPermissions.value?.forEach { 630 revokePermissionsStatusUseCase.invoke(packageName, it.additionalPermission) 631 } 632 _grantedAdditionalPermissions.postValue(emptySet()) 633 } 634 _revokeAllHealthPermissionsState.postValue(RevokeAllState.Updated) 635 _grantedMedicalPermissions.postValue(emptySet()) 636 } 637 return true 638 } catch (ex: Exception) { 639 Log.e(TAG, "Failed to revoke medical permissions!", ex) 640 } 641 return false 642 } 643 644 fun deleteAppData(packageName: String, appName: String) { 645 if (newDeletionFlow) { 646 newDeleteAppData(packageName, appName) 647 } else { 648 oldDeleteAppData(packageName, appName) 649 } 650 } 651 652 private fun newDeleteAppData(packageName: String, appName: String) { 653 viewModelScope.launch { deleteAppDataUseCase.invoke(DeleteAppData(packageName, appName)) } 654 } 655 656 private fun oldDeleteAppData(packageName: String, appName: String) { 657 viewModelScope.launch { 658 val appData = DeletionTypeAppData(packageName, appName) 659 val timeRangeFilter = 660 TimeInstantRangeFilter.Builder() 661 .setStartTime(Instant.EPOCH) 662 .setEndTime(Instant.ofEpochMilli(Long.MAX_VALUE)) 663 .build() 664 oldDeleteAppDataUseCase.invoke(appData, timeRangeFilter) 665 } 666 } 667 668 fun shouldNavigateToAppPermissionsFragment(packageName: String): Boolean { 669 return isPackageSupported(packageName) || hasGrantedPermissions(packageName) 670 } 671 672 private fun hasGrantedPermissions(packageName: String): Boolean { 673 return loadGrantedHealthPermissionsUseCase(packageName) 674 .map { permission -> fromPermissionString(permission) } 675 .isNotEmpty() 676 } 677 678 private fun isAllFitnessPermissionsGranted( 679 permissionsListLiveData: LiveData<List<FitnessPermission>>, 680 grantedPermissionsLiveData: LiveData<Set<FitnessPermission>>, 681 ): Boolean { 682 val permissionsList = permissionsListLiveData.value.orEmpty() 683 val grantedPermissions = grantedPermissionsLiveData.value.orEmpty() 684 return if (permissionsList.isEmpty() || grantedPermissions.isEmpty()) { 685 false 686 } else { 687 permissionsList.size == grantedPermissions.size 688 } 689 } 690 691 private fun isAllMedicalPermissionsGranted( 692 permissionsListLiveData: LiveData<List<MedicalPermission>>, 693 grantedPermissionsLiveData: LiveData<Set<MedicalPermission>>, 694 ): Boolean { 695 val permissionsList = permissionsListLiveData.value.orEmpty() 696 val grantedPermissions = grantedPermissionsLiveData.value.orEmpty() 697 return if (permissionsList.isEmpty() || grantedPermissions.isEmpty()) { 698 false 699 } else { 700 permissionsList.size == grantedPermissions.size 701 } 702 } 703 704 /** Returns True if the packageName declares the Rationale intent, False otherwise */ 705 fun isPackageSupported(packageName: String): Boolean { 706 return healthPermissionReader.isRationaleIntentDeclared(packageName) 707 } 708 709 fun hideExerciseRoutePermissionDialog() { 710 _showDisableExerciseRouteEvent.postValue(false) 711 } 712 713 sealed class RevokeAllState { 714 object NotStarted : RevokeAllState() 715 716 object Loading : RevokeAllState() 717 718 object Updated : RevokeAllState() 719 } 720 721 data class DisableExerciseRouteDialogEvent( 722 val shouldShowDialog: Boolean = false, 723 val appName: String = "", 724 ) 725 } 726