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