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