• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  * ```
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.shared
17 
18 import android.app.AppOpsManager
19 import android.content.Context
20 import android.content.Intent
21 import android.content.pm.PackageInfo
22 import android.content.pm.PackageManager
23 import android.content.pm.PackageManager.NameNotFoundException
24 import android.content.pm.PackageManager.PackageInfoFlags
25 import android.content.pm.PackageManager.ResolveInfoFlags
26 import android.health.connect.HealthConnectManager
27 import android.health.connect.HealthPermissions
28 import android.os.Process
29 import com.android.healthconnect.controller.permissions.api.GetHealthPermissionsFlagsUseCase
30 import com.android.healthconnect.controller.permissions.data.HealthPermission
31 import com.android.healthconnect.controller.permissions.data.HealthPermission.AdditionalPermission
32 import com.android.healthconnect.controller.permissions.data.HealthPermission.Companion.isAdditionalPermission
33 import com.android.healthconnect.controller.permissions.data.HealthPermission.Companion.isFitnessReadPermission
34 import com.android.healthconnect.controller.permissions.data.HealthPermission.Companion.isMedicalReadPermission
35 import com.android.healthconnect.controller.shared.app.AppPermissionsType
36 import com.android.healthfitness.flags.AconfigFlagHelper
37 import com.android.healthfitness.flags.AconfigFlagHelper.isPersonalHealthRecordEnabled
38 import com.android.healthfitness.flags.Flags
39 import com.google.common.annotations.VisibleForTesting
40 import dagger.hilt.android.qualifiers.ApplicationContext
41 import javax.inject.Inject
42 import javax.inject.Singleton
43 
44 /**
45  * Class that reads permissions declared by Health Connect clients as a string array in their XML
46  * resources. See android.health.connect.HealthPermissions
47  */
48 @Singleton
49 class HealthPermissionReader
50 @Inject
51 constructor(
52     @ApplicationContext private val context: Context,
53     private val getHealthPermissionsFlagsUseCase: GetHealthPermissionsFlagsUseCase,
54 ) {
55 
56     companion object {
57         private const val HEALTH_PERMISSION_GROUP = "android.permission-group.HEALTH"
58         private const val RESOLVE_INFO_FLAG: Long = PackageManager.MATCH_ALL.toLong()
59         private const val PACKAGE_INFO_PERMISSIONS_FLAG: Long =
60             PackageManager.GET_PERMISSIONS.toLong()
61 
62         private val medicalPermissions =
63             setOf(
64                 HealthPermissions.WRITE_MEDICAL_DATA,
65                 HealthPermissions.READ_MEDICAL_DATA_ALLERGIES_INTOLERANCES,
66                 HealthPermissions.READ_MEDICAL_DATA_CONDITIONS,
67                 HealthPermissions.READ_MEDICAL_DATA_LABORATORY_RESULTS,
68                 HealthPermissions.READ_MEDICAL_DATA_MEDICATIONS,
69                 HealthPermissions.READ_MEDICAL_DATA_PERSONAL_DETAILS,
70                 HealthPermissions.READ_MEDICAL_DATA_PRACTITIONER_DETAILS,
71                 HealthPermissions.READ_MEDICAL_DATA_PREGNANCY,
72                 HealthPermissions.READ_MEDICAL_DATA_PROCEDURES,
73                 HealthPermissions.READ_MEDICAL_DATA_SOCIAL_HISTORY,
74                 HealthPermissions.READ_MEDICAL_DATA_VACCINES,
75                 HealthPermissions.READ_MEDICAL_DATA_VISITS,
76                 HealthPermissions.READ_MEDICAL_DATA_VITAL_SIGNS,
77             )
78 
79         /**
80          * Determines if an app's permission group is user-sensitive. If an app is not user
81          * sensitive, then it is considered a system app, and hidden in the UI by default.
82          *
83          * This logic is copied from PermissionController/AppPermGroupUiInfoLiveData because we want
84          * to achieve consistent numbers as showed in Settings->PermissionManager.
85          *
86          * @param permFlags the permission flags corresponding to the permissions requested by a
87          *   given app
88          * @param packageFlags flag of
89          *   [android.R.styleable#AndroidManifestUsesPermission&lt;uses-permission&gt;] tag included
90          *   under &lt;manifest&gt
91          * @return Whether or not this package requests a user sensitive permission
92          */
93         private fun isUserSensitive(permFlags: Int?, packageFlags: Int?): Boolean {
94             if (permFlags == null || packageFlags == null) {
95                 return true
96             }
97             val granted =
98                 packageFlags and PackageInfo.REQUESTED_PERMISSION_GRANTED != 0 &&
99                     permFlags and PackageManager.FLAG_PERMISSION_REVOKED_COMPAT == 0
100             return if (granted) {
101                 permFlags and PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED != 0
102             } else {
103                 permFlags and PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED != 0
104             }
105         }
106     }
107 
108     /**
109      * Returns a list of app packageNames that have declared at least one health permission
110      * (additional or data type).
111      *
112      * @return a map of apps to a boolean representing whether this app is a system app
113      */
114     fun getAppsWithHealthPermissions(): Map<String, Boolean> {
115         return if (
116             context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH) &&
117                 Flags.replaceBodySensorPermissionEnabled()
118         ) {
119             // On Wear, do not depend on intent filter, instead, query apps by requested
120             // permissions.
121             getPackagesRequestingSystemHealthPermissions()
122         } else {
123             // On handheld devices, require intent filter or split permission.
124             getPackagesRequestingHealthPermissions()
125         }
126     }
127 
128     /**
129      * Identifies apps that have health permissions requested.
130      *
131      * This function queries all apps and search for non-system apps that have requested at least
132      * one health permissions. This function relies on either health rationale intent filter or
133      * split permission.
134      *
135      * @return a map from app package names that have requested at least one health permission to a
136      *   boolean representing they are system apps
137      */
138     private fun getPackagesRequestingHealthPermissions(): Map<String, Boolean> {
139         val packages =
140             context.packageManager.getInstalledPackagesAsUser(
141                 PackageManager.GET_PERMISSIONS,
142                 Process.myUserHandle().identifier,
143             )
144         // TODO: b/402532889 - Retrieve intent filter from packageInfo
145         val appsWithHealthIntent = appsWithDeclaredIntent()
146         val healthPermissions = getHealthPermissions()
147         val healthApps = mutableMapOf<String, Boolean>()
148 
149         for (info in packages) {
150             val packageName = info.packageName
151             val hasIntentFilter = appsWithHealthIntent.contains(packageName)
152             val requestedPermissions =
153                 info.requestedPermissions?.filter { it in healthPermissions } ?: continue
154             if (requestedPermissions.isEmpty()) continue
155 
156             // Select the permissions we will later check flags for.
157             val permissionsToCheckFlags =
158                 when {
159                     hasIntentFilter -> filterInvalidAdditionalPermissions(requestedPermissions)
160                     Flags.replaceBodySensorPermissionEnabled() &&
161                         requestedPermissions.size == 1 &&
162                         requestedPermissions.contains(HealthPermissions.READ_HEART_RATE) ->
163                         requestedPermissions
164                     Flags.replaceBodySensorPermissionEnabled() &&
165                         requestedPermissions.size == 2 &&
166                         requestedPermissions.contains(HealthPermissions.READ_HEART_RATE) &&
167                         requestedPermissions.contains(
168                             HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND
169                         ) -> requestedPermissions
170                     else -> continue
171                 }
172             if (permissionsToCheckFlags.isEmpty()) continue
173 
174             val permissionToFlags =
175                 getHealthPermissionsFlagsUseCase(packageName, permissionsToCheckFlags)
176 
177             // Check if split permission has right flags.
178             if (
179                 !hasIntentFilter &&
180                     permissionsToCheckFlags.any {
181                         permissionToFlags[it]?.and(
182                             PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED
183                         ) == 0
184                     }
185             )
186                 continue
187 
188             // Check if this app is a system app.
189             val isSystem =
190                 permissionsToCheckFlags.all { permission ->
191                     val index = info.requestedPermissions!!.indexOf(permission)
192                     !isUserSensitive(
193                         permissionToFlags[permission],
194                         info.requestedPermissionsFlags?.getOrNull(index),
195                     )
196                 }
197             healthApps[packageName] = isSystem
198         }
199 
200         return healthApps
201     }
202 
203     /**
204      * Identifies apps that have system health permissions requested.
205      *
206      * This function queries all apps and search for non-system apps that have requested at least
207      * one health permissions. This function does not rely on health rationale intent filter.
208      *
209      * @return a map from app package names that have requested at least one system health
210      *   permission to a boolean representing they are system apps
211      */
212     private fun getPackagesRequestingSystemHealthPermissions(): Map<String, Boolean> {
213         val packages =
214             context.packageManager.getInstalledPackagesAsUser(
215                 PackageManager.GET_PERMISSIONS,
216                 Process.myUserHandle().identifier,
217             )
218         val healthApps = mutableMapOf<String, Boolean>()
219         val systemHealthPermissions = getSystemHealthPermissions()
220 
221         for (info in packages) {
222             val packageName = info.packageName
223             val requestedPermissions = info.requestedPermissions ?: continue
224 
225             // Create a subset of requestedPermissions, where only system health permissions are
226             // included. This is because HealthConnect service enforceValidHealthPermissions before
227             // getPermissionFlags, and we're only interested in system health permissions in
228             // displaying wear UI.
229             val requestedSystemHealthPermissions =
230                 requestedPermissions
231                     .withIndex()
232                     .filter { (_, permissionName) ->
233                         systemHealthPermissions.contains(permissionName)
234                     }
235                     .associate { (index, permissionName) -> index to permissionName }
236             if (requestedSystemHealthPermissions.isEmpty()) {
237                 continue
238             }
239 
240             // Use permission flags to determine whether an app is user-sensitive.
241             // This is a HealthConnect service call to get permission flags.
242             val allPermFlags =
243                 getHealthPermissionsFlagsUseCase(
244                     packageName,
245                     requestedSystemHealthPermissions.values.toList(),
246                 )
247             val isNotSystemApp =
248                 requestedSystemHealthPermissions.any { (index, permissionName) ->
249                     isUserSensitive(
250                         allPermFlags[permissionName],
251                         info.requestedPermissionsFlags?.getOrNull(index),
252                     )
253                 }
254             healthApps.put(packageName, !isNotSystemApp)
255         }
256         return healthApps
257     }
258 
259     /**
260      * Returns whether the app is considered a "split-permission" app (i.e. an app that is only
261      * using health permissions as a result of a split-permission auto-migration of the legacy
262      * body-sensor permission).
263      */
264     public fun isBodySensorSplitPermissionApp(packageName: String): Boolean {
265         if (!Flags.replaceBodySensorPermissionEnabled()) return false
266         return try {
267             val packageInfo =
268                 context.packageManager.getPackageInfo(
269                     packageName,
270                     PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()),
271                 )
272             val requestedPermissions = packageInfo.requestedPermissions?.toList() ?: return false
273             val healthPermissions = getHealthPermissions()
274             val filteredPermissions = requestedPermissions.filter { it in healthPermissions }
275 
276             if (filteredPermissions.isEmpty()) {
277                 return false
278             }
279 
280             val canPotentiallyBeSplitPermissions =
281                 when (filteredPermissions.size) {
282                     1 -> filteredPermissions.contains(HealthPermissions.READ_HEART_RATE)
283                     2 ->
284                         filteredPermissions.contains(HealthPermissions.READ_HEART_RATE) &&
285                             filteredPermissions.contains(
286                                 HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND
287                             )
288                     else -> false
289                 }
290 
291             if (!canPotentiallyBeSplitPermissions) {
292                 return false
293             }
294 
295             getHealthPermissionsFlagsUseCase(packageName, filteredPermissions).values.all { flags ->
296                 flags?.and(PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED) ==
297                     PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED
298             }
299         } catch (e: NameNotFoundException) {
300             false
301         }
302     }
303 
304     fun getAppsWithFitnessPermissions(): List<String> {
305         return try {
306             // TODO: b/400346245 - Should we include the split permission apps?
307             appsWithDeclaredIntent().filter {
308                 getValidHealthPermissions(it)
309                     .filterIsInstance<HealthPermission.FitnessPermission>()
310                     .isNotEmpty()
311             }
312         } catch (e: Exception) {
313             emptyList()
314         }
315     }
316 
317     fun getAppsWithMedicalPermissions(): List<String> {
318         return try {
319             appsWithDeclaredIntent().filter {
320                 getValidHealthPermissions(it)
321                     .filterIsInstance<HealthPermission.MedicalPermission>()
322                     .isNotEmpty()
323             }
324         } catch (e: Exception) {
325             emptyList()
326         }
327     }
328 
329     private fun appsWithDeclaredIntent(): List<String> {
330         return context.packageManager
331             .queryIntentActivities(getRationaleIntent(), ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
332             .map { it.activityInfo.packageName }
333             .distinct()
334     }
335 
336     /**
337      * Identifies apps that have the old permissions declared - they need to update before
338      * continuing to sync with Health Connect.
339      */
340     fun getAppsWithOldHealthPermissions(): List<String> {
341         return try {
342             val oldPermissionsRationale = "androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE"
343             val oldPermissionsMetaDataKey = "health_permissions"
344             val intent = Intent(oldPermissionsRationale)
345             val resolveInfoList =
346                 context.packageManager
347                     .queryIntentActivities(intent, PackageManager.GET_META_DATA)
348                     .filter { resolveInfo -> resolveInfo.activityInfo != null }
349                     .filter { resolveInfo -> resolveInfo.activityInfo.metaData != null }
350                     .filter { resolveInfo ->
351                         resolveInfo.activityInfo.metaData.getInt(oldPermissionsMetaDataKey) != -1
352                     }
353 
354             resolveInfoList.map { it.activityInfo.packageName }.distinct()
355         } catch (e: NameNotFoundException) {
356             emptyList()
357         }
358     }
359 
360     /**
361      * Returns a list of health permissions declared by an app that can be rendered in our UI. This
362      * also filters out invalid additional permissions.
363      */
364     fun getValidHealthPermissions(packageName: String): List<HealthPermission> {
365         return try {
366             filterInvalidAdditionalPermissions(getDeclaredHealthPermissions(packageName))
367                 .mapNotNull { permission -> parsePermission(permission) }
368         } catch (e: NameNotFoundException) {
369             emptyList()
370         }
371     }
372 
373     private fun filterInvalidAdditionalPermissions(
374         declaredPermissions: List<String>
375     ): List<String> {
376         val unfilteredPermissions = declaredPermissions.mapNotNull { parsePermission(it) }
377         val filteredPermissions =
378             if (isPersonalHealthRecordEnabled()) {
379                 maybeFilterOutAdditionalIfNotValid(unfilteredPermissions)
380             } else {
381                 unfilteredPermissions
382             }
383         return filteredPermissions.map { it.toString() }
384     }
385 
386     /**
387      * Filers out invalid additional permissions. READ_HEALTH_DATA_HISTORY is valid if at least one
388      * FITNESS READ permission is declared. READ_HEALTH_DATA_IN_BACKGROUND is valid if at least one
389      * HEALTH READ permission is declared.
390      */
391     @VisibleForTesting
392     fun maybeFilterOutAdditionalIfNotValid(
393         declaredPermissions: List<HealthPermission>
394     ): List<HealthPermission> {
395         val historyReadDeclared =
396             declaredPermissions.filterIsInstance<AdditionalPermission>().any {
397                 it == AdditionalPermission.READ_HEALTH_DATA_HISTORY
398             }
399         val backgroundReadDeclared =
400             declaredPermissions.filterIsInstance<AdditionalPermission>().any {
401                 it == AdditionalPermission.READ_HEALTH_DATA_IN_BACKGROUND
402             }
403         val atLeastOneFitnessReadDeclared = declaredPermissions.any { isFitnessReadPermission(it) }
404         val atLeastOneMedicalReadDeclared = declaredPermissions.any { isMedicalReadPermission(it) }
405         val atLeastOneHealthReadDeclared =
406             atLeastOneFitnessReadDeclared || atLeastOneMedicalReadDeclared
407 
408         var result = declaredPermissions.toMutableList()
409         if (historyReadDeclared && !atLeastOneFitnessReadDeclared) {
410             result =
411                 result
412                     .filterNot { it == AdditionalPermission.READ_HEALTH_DATA_HISTORY }
413                     .toMutableList()
414         }
415         if (backgroundReadDeclared && !atLeastOneHealthReadDeclared) {
416             result =
417                 result
418                     .filterNot { it == AdditionalPermission.READ_HEALTH_DATA_IN_BACKGROUND }
419                     .toMutableList()
420         }
421         return result.toList()
422     }
423 
424     /** Returns a list of health permissions that are declared by an app. */
425     fun getDeclaredHealthPermissions(packageName: String): List<String> {
426         return try {
427             val appInfo =
428                 context.packageManager.getPackageInfo(
429                     packageName,
430                     PackageInfoFlags.of(PACKAGE_INFO_PERMISSIONS_FLAG),
431                 )
432             val healthPermissions = getHealthPermissions()
433             appInfo.requestedPermissions?.filter { it in healthPermissions }.orEmpty()
434         } catch (e: NameNotFoundException) {
435             emptyList()
436         }
437     }
438 
439     fun getAppPermissionsType(packageName: String): AppPermissionsType {
440         val permissions = getValidHealthPermissions(packageName)
441         val hasAtLeastOneFitnessPermission =
442             permissions.firstOrNull { it is HealthPermission.FitnessPermission } != null
443         val hasAtLeastOneMedicalPermission =
444             permissions.firstOrNull { it is HealthPermission.MedicalPermission } != null
445 
446         return if (hasAtLeastOneFitnessPermission && hasAtLeastOneMedicalPermission) {
447             AppPermissionsType.COMBINED_PERMISSIONS
448         } else if (hasAtLeastOneFitnessPermission) {
449             AppPermissionsType.FITNESS_PERMISSIONS_ONLY
450         } else if (hasAtLeastOneMedicalPermission) {
451             AppPermissionsType.MEDICAL_PERMISSIONS_ONLY
452         } else {
453             // All Fitness, Medical and Combined screens handle the empty state so any of those can
454             // be returned here.
455             AppPermissionsType.FITNESS_PERMISSIONS_ONLY
456         }
457     }
458 
459     /**
460      * When PHR flag is on, returns valid additional permissions that we can display in our UI. An
461      * additional permission is valid if the correct read permissions are declared.
462      *
463      * When PHR flag is off, returns additional permissions that are declared.
464      */
465     fun getAdditionalPermissions(packageName: String): List<String> {
466         return if (isPersonalHealthRecordEnabled()) {
467             getValidHealthPermissions(packageName)
468                 .map { it.toString() }
469                 .filter { perm -> isAdditionalPermission(perm) && !shouldHidePermission(perm) }
470         } else {
471             getDeclaredHealthPermissions(packageName).filter { perm ->
472                 isAdditionalPermission(perm) && !shouldHidePermission(perm)
473             }
474         }
475     }
476 
477     fun isRationaleIntentDeclared(packageName: String): Boolean {
478         val intent = getRationaleIntent(packageName)
479         val resolvedInfo =
480             context.packageManager.queryIntentActivities(
481                 intent,
482                 ResolveInfoFlags.of(RESOLVE_INFO_FLAG),
483             )
484         return resolvedInfo.any { info -> info.activityInfo.packageName == packageName }
485     }
486 
487     fun getApplicationRationaleIntent(packageName: String): Intent {
488         val intent = getRationaleIntent(packageName)
489         val resolvedInfo =
490             context.packageManager.queryIntentActivities(
491                 intent,
492                 ResolveInfoFlags.of(RESOLVE_INFO_FLAG),
493             )
494         resolvedInfo.forEach { info -> intent.setClassName(packageName, info.activityInfo.name) }
495         return intent
496     }
497 
498     private fun parsePermission(permission: String): HealthPermission? {
499         return try {
500             HealthPermission.fromPermissionString(permission)
501         } catch (e: IllegalArgumentException) {
502             null
503         }
504     }
505 
506     /** Returns a list of all health permissions in the HEALTH permission group. */
507     @VisibleForTesting
508     fun getHealthPermissions(): List<String> {
509         val permissions =
510             context.packageManager.queryPermissionsByGroup(HEALTH_PERMISSION_GROUP, 0).map {
511                 permissionInfo ->
512                 permissionInfo.name
513             }
514         return permissions.filterNot { permission -> shouldHidePermission(permission) }
515     }
516 
517     /** Returns a list of all system health permissions in the HEALTH permission group. */
518     fun getSystemHealthPermissions(): List<String> {
519         val permissions =
520             context.packageManager
521                 .queryPermissionsByGroup(HEALTH_PERMISSION_GROUP, 0)
522                 .map { permissionInfo -> permissionInfo.name }
523                 .filter { permissionName ->
524                     val appOp = AppOpsManager.permissionToOp(permissionName)
525                     appOp != null && !appOp.equals(AppOpsManager.OPSTR_READ_WRITE_HEALTH_DATA)
526                 }
527         return permissions
528     }
529 
530     fun shouldHidePermission(permission: String): Boolean {
531         return when (permission) {
532             in medicalPermissions -> !isPersonalHealthRecordEnabled()
533             HealthPermissions.READ_ACTIVITY_INTENSITY,
534             HealthPermissions.WRITE_ACTIVITY_INTENSITY ->
535                 !AconfigFlagHelper.isActivityIntensityEnabled()
536             else -> false
537         }
538     }
539 
540     private fun getRationaleIntent(packageName: String? = null): Intent {
541         val intent =
542             Intent(Intent.ACTION_VIEW_PERMISSION_USAGE).apply {
543                 addCategory(HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS)
544                 if (packageName != null) {
545                     setPackage(packageName)
546                 }
547             }
548         return intent
549     }
550 }
551