• 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.content.Context
19 import android.content.Intent
20 import android.content.pm.PackageManager
21 import android.content.pm.PackageManager.NameNotFoundException
22 import android.content.pm.PackageManager.PackageInfoFlags
23 import android.content.pm.PackageManager.ResolveInfoFlags
24 import android.health.connect.HealthConnectManager
25 import android.health.connect.HealthPermissions
26 import com.android.healthconnect.controller.permissions.data.HealthPermission
27 import com.android.healthconnect.controller.utils.FeatureUtils
28 import dagger.hilt.android.qualifiers.ApplicationContext
29 import javax.inject.Inject
30 
31 /**
32  * Class that read permissions declared by Health Connect clients as a string array in their XML
33  * resources. see android.health.connect.HealthPermissions
34  */
35 class HealthPermissionReader
36 @Inject
37 constructor(
38     @ApplicationContext private val context: Context,
39     private val featureUtils: FeatureUtils
40 ) {
41 
42     companion object {
43         private const val RESOLVE_INFO_FLAG: Long = PackageManager.MATCH_ALL.toLong()
44         private const val PACKAGE_INFO_PERMISSIONS_FLAG: Long =
45             PackageManager.GET_PERMISSIONS.toLong()
46         private val sessionTypePermissions =
47             listOf(
48                 HealthPermissions.READ_EXERCISE,
49                 HealthPermissions.WRITE_EXERCISE,
50                 HealthPermissions.READ_SLEEP,
51                 HealthPermissions.WRITE_SLEEP,
52             )
53         private val exerciseRoutePermissions =
54             listOf(
55                 HealthPermissions.WRITE_EXERCISE_ROUTE,
56             )
57     }
58 
59     suspend fun getAppsWithHealthPermissions(): List<String> {
60         return try {
61             val appsWithDeclaredIntent =
62                 context.packageManager
63                     .queryIntentActivities(
64                         getRationaleIntent(), ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
65                     .map { it.activityInfo.packageName }
66 
67             appsWithDeclaredIntent.filter { getDeclaredPermissions(it).isNotEmpty() }
68         } catch (e: Exception) {
69             emptyList()
70         }
71     }
72 
73     suspend fun getDeclaredPermissions(packageName: String): List<HealthPermission> {
74         return try {
75             val appInfo =
76                 context.packageManager.getPackageInfo(
77                     packageName, PackageInfoFlags.of(PACKAGE_INFO_PERMISSIONS_FLAG))
78             val healthPermissions = getHealthPermissions()
79             appInfo.requestedPermissions
80                 ?.filter { it in healthPermissions }
81                 ?.map { permission -> parsePermission(permission) }
82                 .orEmpty()
83         } catch (e: NameNotFoundException) {
84             emptyList()
85         }
86     }
87 
88     fun isRationalIntentDeclared(packageName: String): Boolean {
89         val intent = getRationaleIntent(packageName)
90         val resolvedInfo =
91             context.packageManager.queryIntentActivities(
92                 intent, ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
93         return resolvedInfo.any { info -> info.activityInfo.packageName == packageName }
94     }
95 
96     fun getApplicationRationaleIntent(packageName: String): Intent {
97         val intent = getRationaleIntent(packageName)
98         val resolvedInfo =
99             context.packageManager.queryIntentActivities(
100                 intent, ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
101         resolvedInfo.forEach { info -> intent.setClassName(packageName, info.activityInfo.name) }
102         return intent
103     }
104 
105     private fun parsePermission(permission: String): HealthPermission {
106         return HealthPermission.fromPermissionString(permission)
107     }
108 
109     private fun getHealthPermissions(): List<String> {
110         val permissions =
111             context.packageManager
112                 .queryPermissionsByGroup("android.permission-group.HEALTH", 0)
113                 .map { permissionInfo -> permissionInfo.name }
114         return permissions.filterNot { permission ->
115             shouldHideExerciseRoute(permission) || shouldHideSessionTypes(permission)
116         }
117     }
118 
119     private fun shouldHideExerciseRoute(permission: String): Boolean {
120         return permission in exerciseRoutePermissions && !featureUtils.isExerciseRouteEnabled()
121     }
122 
123     private fun shouldHideSessionTypes(permission: String): Boolean {
124         return permission in sessionTypePermissions && !featureUtils.isSessionTypesEnabled()
125     }
126 
127     private fun getRationaleIntent(packageName: String? = null): Intent {
128         val intent =
129             Intent(Intent.ACTION_VIEW_PERMISSION_USAGE).apply {
130                 addCategory(HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS)
131                 if (packageName != null) {
132                     setPackage(packageName)
133                 }
134             }
135         return intent
136     }
137 }
138