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 com.google.common.annotations.VisibleForTesting
29 import dagger.hilt.android.qualifiers.ApplicationContext
30 import javax.inject.Inject
31 import javax.inject.Singleton
32 
33 /**
34  * Class that reads permissions declared by Health Connect clients as a string array in their XML
35  * resources. See android.health.connect.HealthPermissions
36  */
37 @Singleton
38 class HealthPermissionReader
39 @Inject
40 constructor(
41     @ApplicationContext private val context: Context,
42     private val featureUtils: FeatureUtils
43 ) {
44 
45     companion object {
46         private const val RESOLVE_INFO_FLAG: Long = PackageManager.MATCH_ALL.toLong()
47         private const val PACKAGE_INFO_PERMISSIONS_FLAG: Long =
48             PackageManager.GET_PERMISSIONS.toLong()
49         private val sessionTypePermissions =
50             listOf(
51                 HealthPermissions.READ_EXERCISE,
52                 HealthPermissions.WRITE_EXERCISE,
53                 HealthPermissions.READ_SLEEP,
54                 HealthPermissions.WRITE_SLEEP,
55             )
56 
57         private val backgroundReadPermission =
58             listOf(HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND)
59 
60         private val historyReadPermission = listOf(HealthPermissions.READ_HEALTH_DATA_HISTORY)
61 
62         /** Special health permissions that don't represent health data types. */
63         private val additionalPermissions =
64             setOf(
65                 HealthPermissions.READ_EXERCISE_ROUTES,
66                 HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
67                 HealthPermissions.READ_HEALTH_DATA_HISTORY)
68 
69         private val medicalPermissions =
70             setOf(
71                 HealthPermissions.WRITE_MEDICAL_DATA,
72                 HealthPermissions.READ_MEDICAL_DATA_IMMUNIZATION)
73     }
74 
75     /**
76      * Returns a list of app packageNames that have declared at least one health permission
77      * (additional or data type).
78      */
79     fun getAppsWithHealthPermissions(): List<String> {
80         return try {
81             val appsWithDeclaredIntent =
82                 context.packageManager
83                     .queryIntentActivities(
84                         getRationaleIntent(), ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
85                     .map { it.activityInfo.packageName }
86                     .distinct()
87 
88             appsWithDeclaredIntent.filter { getValidHealthPermissions(it).isNotEmpty() }
89         } catch (e: Exception) {
90             emptyList()
91         }
92     }
93 
94     fun getAppsWithFitnessPermissions(): List<String> {
95         return try {
96             val appsWithDeclaredIntent =
97                 context.packageManager
98                     .queryIntentActivities(
99                         getRationaleIntent(), ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
100                     .map { it.activityInfo.packageName }
101                     .distinct()
102 
103             appsWithDeclaredIntent.filter {
104                 getValidHealthPermissions(it)
105                     .filterIsInstance<HealthPermission.FitnessPermission>()
106                     .isNotEmpty()
107             }
108         } catch (e: Exception) {
109             emptyList()
110         }
111     }
112 
113     /**
114      * Identifies apps that have the old permissions declared - they need to update before
115      * continuing to sync with Health Connect.
116      */
117     fun getAppsWithOldHealthPermissions(): List<String> {
118         return try {
119             val oldPermissionsRationale = "androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE"
120             val oldPermissionsMetaDataKey = "health_permissions"
121             val intent = Intent(oldPermissionsRationale)
122             val resolveInfoList =
123                 context.packageManager
124                     .queryIntentActivities(intent, PackageManager.GET_META_DATA)
125                     .filter { resolveInfo -> resolveInfo.activityInfo != null }
126                     .filter { resolveInfo -> resolveInfo.activityInfo.metaData != null }
127                     .filter { resolveInfo ->
128                         resolveInfo.activityInfo.metaData.getInt(oldPermissionsMetaDataKey) != -1
129                     }
130 
131             resolveInfoList.map { it.activityInfo.packageName }.distinct()
132         } catch (e: NameNotFoundException) {
133             emptyList()
134         }
135     }
136 
137     /** Returns a list of health permissions declared by an app that can be rendered in our UI. */
138     fun getValidHealthPermissions(packageName: String): List<HealthPermission> {
139         return try {
140             val permissions = getDeclaredHealthPermissions(packageName)
141             permissions.mapNotNull { permission -> parsePermission(permission) }
142         } catch (e: NameNotFoundException) {
143             emptyList()
144         }
145     }
146 
147     /** Returns a list of health permissions that are declared by an app. */
148     fun getDeclaredHealthPermissions(packageName: String): List<String> {
149         return try {
150             val appInfo =
151                 context.packageManager.getPackageInfo(
152                     packageName, PackageInfoFlags.of(PACKAGE_INFO_PERMISSIONS_FLAG))
153             val healthPermissions = getHealthPermissions()
154             appInfo.requestedPermissions?.filter { it in healthPermissions }.orEmpty()
155         } catch (e: NameNotFoundException) {
156             emptyList()
157         }
158     }
159 
160     fun getAdditionalPermissions(packageName: String): List<String> {
161         return getDeclaredHealthPermissions(packageName).filter { perm ->
162             isAdditionalPermission(perm) && !shouldHidePermission(perm)
163         }
164     }
165 
166     fun isRationaleIntentDeclared(packageName: String): Boolean {
167         val intent = getRationaleIntent(packageName)
168         val resolvedInfo =
169             context.packageManager.queryIntentActivities(
170                 intent, ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
171         return resolvedInfo.any { info -> info.activityInfo.packageName == packageName }
172     }
173 
174     fun getApplicationRationaleIntent(packageName: String): Intent {
175         val intent = getRationaleIntent(packageName)
176         val resolvedInfo =
177             context.packageManager.queryIntentActivities(
178                 intent, ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
179         resolvedInfo.forEach { info -> intent.setClassName(packageName, info.activityInfo.name) }
180         return intent
181     }
182 
183     private fun parsePermission(permission: String): HealthPermission? {
184         return try {
185             HealthPermission.fromPermissionString(permission)
186         } catch (e: IllegalArgumentException) {
187             null
188         }
189     }
190 
191     /** Returns a list of all health permissions in the HEALTH permission group. */
192     @VisibleForTesting
193     fun getHealthPermissions(): List<String> {
194         val permissions =
195             context.packageManager
196                 .queryPermissionsByGroup("android.permission-group.HEALTH", 0)
197                 .map { permissionInfo -> permissionInfo.name }
198         return permissions.filterNot { permission -> shouldHidePermission(permission) }
199     }
200 
201     fun isAdditionalPermission(permission: String): Boolean {
202         return additionalPermissions.contains(permission)
203     }
204 
205     fun isMedicalPermission(permission: String): Boolean {
206         return medicalPermissions.contains(permission)
207     }
208 
209     fun isFitnessPermission(permission: String): Boolean {
210         return !isAdditionalPermission(permission) && !isMedicalPermission(permission)
211     }
212 
213     fun shouldHidePermission(permission: String): Boolean {
214         return shouldHideSessionTypes(permission) ||
215             shouldHideBackgroundReadPermission(permission) ||
216             shouldHideSkinTemperaturePermissions(permission) ||
217             shouldHidePlannedExercisePermissions(permission) ||
218             shouldHideMindfulnessSessionPermissions(permission) ||
219             shouldHideHistoryReadPermission(permission) ||
220             shouldHideMedicalPermission(permission)
221     }
222 
223     private fun shouldHideSkinTemperaturePermissions(permission: String): Boolean {
224         return (permission == HealthPermissions.READ_SKIN_TEMPERATURE ||
225             permission == HealthPermissions.WRITE_SKIN_TEMPERATURE) &&
226             !featureUtils.isSkinTemperatureEnabled()
227     }
228 
229     private fun shouldHidePlannedExercisePermissions(permission: String): Boolean {
230         return (permission == HealthPermissions.READ_PLANNED_EXERCISE ||
231             permission == HealthPermissions.WRITE_PLANNED_EXERCISE) &&
232             !featureUtils.isPlannedExerciseEnabled()
233     }
234 
235     private fun shouldHideMindfulnessSessionPermissions(permission: String): Boolean {
236         return permission == HealthPermissions.READ_MINDFULNESS ||
237             permission == HealthPermissions.WRITE_MINDFULNESS
238     }
239 
240     private fun shouldHideSessionTypes(permission: String): Boolean {
241         return permission in sessionTypePermissions && !featureUtils.isSessionTypesEnabled()
242     }
243 
244     private fun shouldHideBackgroundReadPermission(permission: String): Boolean {
245         return permission in backgroundReadPermission && !featureUtils.isBackgroundReadEnabled()
246     }
247 
248     private fun shouldHideHistoryReadPermission(permission: String): Boolean {
249         return permission in historyReadPermission && !featureUtils.isHistoryReadEnabled()
250     }
251 
252     private fun shouldHideMedicalPermission(permission: String): Boolean {
253         return permission in medicalPermissions && !featureUtils.isPersonalHealthRecordEnabled()
254     }
255 
256     private fun getRationaleIntent(packageName: String? = null): Intent {
257         val intent =
258             Intent(Intent.ACTION_VIEW_PERMISSION_USAGE).apply {
259                 addCategory(HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS)
260                 if (packageName != null) {
261                     setPackage(packageName)
262                 }
263             }
264         return intent
265     }
266 }
267