1 /*
<lambda>null2 * Copyright (C) 2024 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.graphics.drawable.Drawable
20 import android.health.connect.HealthDataCategory
21 import android.health.connect.internal.datatypes.utils.HealthConnectMappings
22 import androidx.annotation.StringRes
23 import com.android.healthconnect.controller.R
24 import com.android.healthconnect.controller.permissions.data.FitnessPermissionType
25 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
26 import com.android.healthconnect.controller.permissions.data.MedicalPermissionType
27 import com.android.healthconnect.controller.permissions.data.fromHealthPermissionCategory
28 import com.android.healthconnect.controller.shared.CategoriesMappers.ACTIVITY_PERMISSION_GROUPS
29 import com.android.healthconnect.controller.shared.CategoriesMappers.BODY_MEASUREMENTS_PERMISSION_GROUPS
30 import com.android.healthconnect.controller.shared.CategoriesMappers.CYCLE_TRACKING_PERMISSION_GROUPS
31 import com.android.healthconnect.controller.shared.CategoriesMappers.NUTRITION_PERMISSION_GROUPS
32 import com.android.healthconnect.controller.shared.CategoriesMappers.SLEEP_PERMISSION_GROUPS
33 import com.android.healthconnect.controller.shared.CategoriesMappers.VITALS_PERMISSION_GROUPS
34 import com.android.healthconnect.controller.shared.CategoriesMappers.WELLNESS_PERMISSION_GROUPS
35 import com.android.healthconnect.controller.utils.AttributeResolver
36 import com.android.healthfitness.flags.Flags
37
38 object HealthDataCategoryExtensions {
39 /** Additional category for medical permission types. */
40 const val MEDICAL = 1000
41
42 private val DATA_CATEGORY_TO_HEALTH_PERMISSION_TYPE_MAP =
43 createDataCategoryToHealthPermissionTypeMap()
44
45 private fun createDataCategoryToHealthPermissionTypeMap():
46 Map<Int, List<HealthPermissionType>> {
47
48 if (!Flags.healthConnectMappings()) {
49 return emptyMap()
50 }
51
52 val specialCases =
53 mapOf(
54 HealthDataCategory.ACTIVITY to listOf(FitnessPermissionType.EXERCISE_ROUTE),
55 MEDICAL to MedicalPermissionType.entries,
56 )
57
58 val healthConnectMappings = HealthConnectMappings.getInstance()
59
60 return healthConnectMappings.allRecordTypeIdentifiers
61 .map { recordTypeId ->
62 healthConnectMappings.getRecordCategoryForRecordType(recordTypeId) to
63 healthConnectMappings.getHealthPermissionCategoryForRecordType(recordTypeId)
64 }
65 .groupBy({ it.first }, { fromHealthPermissionCategory(it.second) })
66 .toMutableMap()
67 .apply { specialCases.forEach { merge(it.key, it.value) { a, b -> a + b } } }
68 .mapValues { it.value.distinct() }
69 .toMap()
70 }
71
72 fun @receiver:HealthDataCategoryInt Int.healthPermissionTypes(): List<HealthPermissionType> {
73 if (!Flags.healthConnectMappings()) {
74 return this.healthPermissionTypesLegacy()
75 }
76
77 return DATA_CATEGORY_TO_HEALTH_PERMISSION_TYPE_MAP[this]
78 ?: throw IllegalArgumentException("Category $this is not supported.")
79 }
80
81 private fun @receiver:HealthDataCategoryInt Int.healthPermissionTypesLegacy():
82 List<HealthPermissionType> {
83 return when (this) {
84 HealthDataCategory.ACTIVITY -> ACTIVITY_PERMISSION_GROUPS
85 HealthDataCategory.BODY_MEASUREMENTS -> BODY_MEASUREMENTS_PERMISSION_GROUPS
86 HealthDataCategory.CYCLE_TRACKING -> CYCLE_TRACKING_PERMISSION_GROUPS
87 HealthDataCategory.NUTRITION -> NUTRITION_PERMISSION_GROUPS
88 HealthDataCategory.SLEEP -> SLEEP_PERMISSION_GROUPS
89 HealthDataCategory.VITALS -> VITALS_PERMISSION_GROUPS
90 HealthDataCategory.WELLNESS -> WELLNESS_PERMISSION_GROUPS
91 MEDICAL -> MedicalPermissionType.entries
92 else -> throw IllegalArgumentException("Category $this is not supported.")
93 }
94 }
95
96 @StringRes
97 fun @receiver:HealthDataCategoryInt Int.lowercaseTitle(): Int {
98 return when (this) {
99 HealthDataCategory.ACTIVITY -> R.string.activity_category_lowercase
100 HealthDataCategory.BODY_MEASUREMENTS -> R.string.body_measurements_category_lowercase
101 HealthDataCategory.CYCLE_TRACKING -> R.string.cycle_tracking_category_lowercase
102 HealthDataCategory.NUTRITION -> R.string.nutrition_category_lowercase
103 HealthDataCategory.SLEEP -> R.string.sleep_category_lowercase
104 HealthDataCategory.VITALS -> R.string.vitals_category_lowercase
105 HealthDataCategory.WELLNESS -> R.string.wellness_category_lowercase
106 MEDICAL -> R.string.medical_permissions_lowercase
107 else -> throw IllegalArgumentException("Category $this is not supported.")
108 }
109 }
110
111 @StringRes
112 fun @receiver:HealthDataCategoryInt Int.uppercaseTitle(): Int {
113 return when (this) {
114 HealthDataCategory.ACTIVITY -> R.string.activity_category_uppercase
115 HealthDataCategory.BODY_MEASUREMENTS -> R.string.body_measurements_category_uppercase
116 HealthDataCategory.CYCLE_TRACKING -> R.string.cycle_tracking_category_uppercase
117 HealthDataCategory.NUTRITION -> R.string.nutrition_category_uppercase
118 HealthDataCategory.SLEEP -> R.string.sleep_category_uppercase
119 HealthDataCategory.VITALS -> R.string.vitals_category_uppercase
120 HealthDataCategory.WELLNESS -> R.string.wellness_category_uppercase
121 MEDICAL -> R.string.medical_permissions
122 else -> throw IllegalArgumentException("Category $this is not supported.")
123 }
124 }
125
126 fun @receiver:HealthDataCategoryInt Int.icon(context: Context): Drawable? {
127 val attrRes: Int =
128 when (this) {
129 HealthDataCategory.ACTIVITY -> R.attr.activityCategoryIcon
130 HealthDataCategory.BODY_MEASUREMENTS -> R.attr.bodyMeasurementsCategoryIcon
131 HealthDataCategory.CYCLE_TRACKING -> R.attr.cycleTrackingCategoryIcon
132 HealthDataCategory.NUTRITION -> R.attr.nutritionCategoryIcon
133 HealthDataCategory.SLEEP -> R.attr.sleepCategoryIcon
134 HealthDataCategory.VITALS -> R.attr.vitalsCategoryIcon
135 HealthDataCategory.WELLNESS -> R.attr.wellnessCategoryIcon
136 // TODO(b/342156345): Add default medical icon.
137 MEDICAL -> R.attr.vitalsCategoryIcon
138 else -> throw IllegalArgumentException("Category $this is not supported.")
139 }
140 return AttributeResolver.getDrawable(context, attrRes)
141 }
142
143 @HealthDataCategoryInt
144 fun fromFitnessPermissionType(type: FitnessPermissionType): Int {
145 val result = safelyFromFitnessPermissionType(type)
146 return result
147 ?: throw IllegalArgumentException("No Category for fitness permission type $type")
148 }
149
150 @HealthDataCategoryInt
151 fun safelyFromFitnessPermissionType(type: FitnessPermissionType): Int? {
152 return getAllFitnessDataCategories().firstOrNull {
153 it.healthPermissionTypes().contains(type)
154 }
155 }
156 }
157
158 /** Permission groups for each {@link HealthDataCategory}. */
159 private object CategoriesMappers {
160 val ACTIVITY_PERMISSION_GROUPS =
161 listOf(
162 FitnessPermissionType.ACTIVE_CALORIES_BURNED,
163 FitnessPermissionType.DISTANCE,
164 FitnessPermissionType.ELEVATION_GAINED,
165 FitnessPermissionType.EXERCISE,
166 FitnessPermissionType.EXERCISE_ROUTE,
167 FitnessPermissionType.FLOORS_CLIMBED,
168 FitnessPermissionType.POWER,
169 FitnessPermissionType.SPEED,
170 FitnessPermissionType.STEPS,
171 FitnessPermissionType.TOTAL_CALORIES_BURNED,
172 FitnessPermissionType.VO2_MAX,
173 FitnessPermissionType.WHEELCHAIR_PUSHES,
174 FitnessPermissionType.PLANNED_EXERCISE,
175 )
176
177 val BODY_MEASUREMENTS_PERMISSION_GROUPS =
178 listOf(
179 FitnessPermissionType.BASAL_METABOLIC_RATE,
180 FitnessPermissionType.BODY_FAT,
181 FitnessPermissionType.BODY_WATER_MASS,
182 FitnessPermissionType.BONE_MASS,
183 FitnessPermissionType.HEIGHT,
184 FitnessPermissionType.LEAN_BODY_MASS,
185 FitnessPermissionType.WEIGHT,
186 )
187
188 val CYCLE_TRACKING_PERMISSION_GROUPS =
189 listOf(
190 FitnessPermissionType.CERVICAL_MUCUS,
191 FitnessPermissionType.INTERMENSTRUAL_BLEEDING,
192 FitnessPermissionType.MENSTRUATION,
193 FitnessPermissionType.OVULATION_TEST,
194 FitnessPermissionType.SEXUAL_ACTIVITY,
195 )
196
197 val NUTRITION_PERMISSION_GROUPS =
198 listOf(FitnessPermissionType.HYDRATION, FitnessPermissionType.NUTRITION)
199
200 val SLEEP_PERMISSION_GROUPS = listOf(FitnessPermissionType.SLEEP)
201
202 val VITALS_PERMISSION_GROUPS =
203 listOf(
204 FitnessPermissionType.BASAL_BODY_TEMPERATURE,
205 FitnessPermissionType.BLOOD_GLUCOSE,
206 FitnessPermissionType.BLOOD_PRESSURE,
207 FitnessPermissionType.BODY_TEMPERATURE,
208 FitnessPermissionType.HEART_RATE,
209 FitnessPermissionType.HEART_RATE_VARIABILITY,
210 FitnessPermissionType.OXYGEN_SATURATION,
211 FitnessPermissionType.RESPIRATORY_RATE,
212 FitnessPermissionType.RESTING_HEART_RATE,
213 FitnessPermissionType.SKIN_TEMPERATURE,
214 )
215
216 val WELLNESS_PERMISSION_GROUPS = listOf(FitnessPermissionType.MINDFULNESS)
217 }
218
219 /** List of available Health data categories. */
220 val FITNESS_DATA_CATEGORIES = getAllFitnessDataCategories()
221
222 /**
223 * List of available Health data categories.
224 *
225 * Allows code being unit tested with different flag values.
226 */
getAllFitnessDataCategoriesnull227 fun getAllFitnessDataCategories() =
228 if (Flags.healthConnectMappings()) HealthConnectMappings.getInstance().allHealthDataCategories
229 else getAllFitnessDataCategoriesLegacy()
230
231 fun getAllFitnessDataCategoriesLegacy() =
232 listOf(
233 HealthDataCategory.ACTIVITY,
234 HealthDataCategory.BODY_MEASUREMENTS,
235 HealthDataCategory.CYCLE_TRACKING,
236 HealthDataCategory.NUTRITION,
237 HealthDataCategory.SLEEP,
238 HealthDataCategory.VITALS,
239 HealthDataCategory.WELLNESS,
240 )
241
242 /** Denotes that the annotated [Integer] represents a [HealthDataCategory]. */
243 @Retention(AnnotationRetention.BINARY)
244 @Target(
245 AnnotationTarget.VALUE_PARAMETER,
246 AnnotationTarget.FUNCTION,
247 AnnotationTarget.PROPERTY_GETTER,
248 AnnotationTarget.PROPERTY_SETTER,
249 AnnotationTarget.LOCAL_VARIABLE,
250 AnnotationTarget.FIELD,
251 AnnotationTarget.TYPE,
252 )
253 annotation class HealthDataCategoryInt
254