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<uses-permission>] tag included 90 * under <manifest> 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