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