1 /* <lambda>null2 * Copyright (C) 2023 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.data.appdata 17 18 import android.health.connect.HealthConnectManager 19 import android.health.connect.MedicalResourceTypeInfo 20 import android.health.connect.RecordTypeInfoResponse 21 import android.health.connect.datatypes.Record 22 import android.util.Log 23 import androidx.core.os.asOutcomeReceiver 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.permissions.data.fromMedicalResourceType 29 import com.android.healthconnect.controller.service.IoDispatcher 30 import com.android.healthconnect.controller.shared.FITNESS_DATA_CATEGORIES 31 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.MEDICAL 32 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.healthPermissionTypes 33 import com.android.healthconnect.controller.shared.HealthDataCategoryInt 34 import com.android.healthconnect.controller.shared.usecase.UseCaseResults 35 import javax.inject.Inject 36 import javax.inject.Singleton 37 import kotlinx.coroutines.CoroutineDispatcher 38 import kotlinx.coroutines.suspendCancellableCoroutine 39 import kotlinx.coroutines.withContext 40 41 @Singleton 42 class AllDataUseCase 43 @Inject 44 constructor( 45 private val healthConnectManager: HealthConnectManager, 46 @IoDispatcher private val dispatcher: CoroutineDispatcher, 47 ) { 48 49 /** Returns list of all fitness categories and permission types to be shown on the HC UI. */ 50 suspend fun loadAllFitnessData(): UseCaseResults<List<PermissionTypesPerCategory>> = 51 withContext(dispatcher) { 52 try { 53 val recordTypeInfoMap = getRecordTypeInfoMap() 54 val categories = 55 FITNESS_DATA_CATEGORIES.map { 56 PermissionTypesPerCategory( 57 it, 58 getPermissionTypesPerCategory(it, recordTypeInfoMap, packageName = null), 59 ) 60 } 61 UseCaseResults.Success(categories) 62 } catch (e: Exception) { 63 Log.e("TAG_ERROR", "Loading error ", e) 64 UseCaseResults.Failed(e) 65 } 66 } 67 68 /** Returns list of all medical permission types to be shown on the HC UI. */ 69 suspend fun loadAllMedicalData(): UseCaseResults<List<PermissionTypesPerCategory>> = 70 withContext(dispatcher) { 71 try { 72 val medicalResourceTypeInfos = getMedicalResourceTypeInfos() 73 val medicalPermissionTypes = 74 medicalResourceTypeInfos 75 .filter { it.contributingDataSources.isNotEmpty() } 76 .map { fromMedicalResourceType(it.medicalResourceType) } 77 if (medicalPermissionTypes.isEmpty()) { 78 UseCaseResults.Success(listOf()) 79 } else { 80 UseCaseResults.Success( 81 listOf(PermissionTypesPerCategory(MEDICAL, medicalPermissionTypes)) 82 ) 83 } 84 } catch (e: Exception) { 85 Log.e("TAG_ERROR", "Loading error ", e) 86 UseCaseResults.Failed(e) 87 } 88 } 89 90 /** Returns whether there is any fitness data in HC. */ 91 suspend fun loadHasAnyFitnessData(): UseCaseResults<Boolean> = 92 withContext(dispatcher) { 93 try { 94 val recordTypeInfoMap = getRecordTypeInfoMap() 95 val anyFitnessData = 96 recordTypeInfoMap.any { it.value.contributingPackages.isNotEmpty() } 97 UseCaseResults.Success(anyFitnessData) 98 } catch (e: Exception) { 99 Log.e("TAG_ERROR", "Loading error ", e) 100 UseCaseResults.Failed(e) 101 } 102 } 103 104 /** Returns whether there is any medical data in HC. */ 105 suspend fun loadHasAnyMedicalData(): UseCaseResults<Boolean> = 106 withContext(dispatcher) { 107 try { 108 val medicalResourceTypeInfos = getMedicalResourceTypeInfos() 109 val anyMedicalData = 110 medicalResourceTypeInfos.any { it.contributingDataSources.isNotEmpty() } 111 UseCaseResults.Success(anyMedicalData) 112 } catch (e: Exception) { 113 Log.e("TAG_ERROR", "Loading error ", e) 114 UseCaseResults.Failed(e) 115 } 116 } 117 118 /** 119 * Returns list of fitness categories and permission types written by the given app to be shown 120 * on the HC UI. 121 */ 122 suspend fun loadFitnessAppData( 123 packageName: String 124 ): UseCaseResults<List<PermissionTypesPerCategory>> = 125 withContext(dispatcher) { 126 try { 127 val recordTypeInfoMap = getRecordTypeInfoMap() 128 val categories = 129 FITNESS_DATA_CATEGORIES.map { 130 PermissionTypesPerCategory( 131 it, 132 getPermissionTypesPerCategory(it, recordTypeInfoMap, packageName), 133 ) 134 } 135 UseCaseResults.Success(categories) 136 } catch (e: Exception) { 137 UseCaseResults.Failed(e) 138 } 139 } 140 141 /** 142 * Returns list of medical categories and permission types written by the given app to be shown 143 * on the HC UI. 144 */ 145 suspend fun loadMedicalAppData( 146 packageName: String 147 ): UseCaseResults<List<PermissionTypesPerCategory>> = 148 withContext(dispatcher) { 149 try { 150 val medicalResourceTypeInfos = getMedicalResourceTypeInfos() 151 val medicalPermissionTypes = 152 filterMedicalPermissionTypes(medicalResourceTypeInfos, packageName) 153 if (medicalPermissionTypes.isEmpty()) { 154 UseCaseResults.Success(listOf()) 155 } else { 156 UseCaseResults.Success( 157 listOf(PermissionTypesPerCategory(MEDICAL, medicalPermissionTypes)) 158 ) 159 } 160 } catch (e: Exception) { 161 UseCaseResults.Failed(e) 162 } 163 } 164 165 private suspend fun getRecordTypeInfoMap(): Map<Class<out Record>, RecordTypeInfoResponse> { 166 val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> = 167 suspendCancellableCoroutine { continuation -> 168 healthConnectManager.queryAllRecordTypesInfo( 169 Runnable::run, 170 continuation.asOutcomeReceiver(), 171 ) 172 } 173 return recordTypeInfoMap 174 } 175 176 private suspend fun getMedicalResourceTypeInfos() = 177 suspendCancellableCoroutine { continuation -> 178 healthConnectManager.queryAllMedicalResourceTypeInfos( 179 Runnable::run, 180 continuation.asOutcomeReceiver(), 181 ) 182 } 183 184 private fun filterMedicalPermissionTypes( 185 medicalResourceTypeInfos: List<MedicalResourceTypeInfo>, 186 packageName: String, 187 ): List<MedicalPermissionType> = 188 medicalResourceTypeInfos 189 .filter { medicalResourceTypeInfo -> 190 val contributingPackageNames = 191 medicalResourceTypeInfo.contributingDataSources.map { it.packageName }.toSet() 192 contributingPackageNames.contains(packageName) 193 } 194 .map { fromMedicalResourceType(it.medicalResourceType) } 195 196 /** 197 * Returns those [FitnessPermissionType]s that have some data written by the given [packageName] 198 * app. If the is no app provided then return all data. 199 */ 200 private fun getPermissionTypesPerCategory( 201 category: @HealthDataCategoryInt Int, 202 recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>, 203 packageName: String?, 204 ): List<HealthPermissionType> { 205 if (packageName == null) { 206 return category.healthPermissionTypes().filter { hasData(it, recordTypeInfoMap) } 207 } 208 return category.healthPermissionTypes().filter { 209 hasDataByApp(it, recordTypeInfoMap, packageName) 210 } 211 } 212 213 private fun hasData( 214 permissionType: HealthPermissionType, 215 recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>, 216 ): Boolean = 217 recordTypeInfoMap.values.firstOrNull { 218 fromHealthPermissionCategory(it.permissionCategory) == permissionType && 219 it.contributingPackages.isNotEmpty() 220 } != null 221 222 private fun hasDataByApp( 223 permissionType: HealthPermissionType, 224 recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>, 225 packageName: String, 226 ): Boolean = 227 recordTypeInfoMap.values.firstOrNull { 228 fromHealthPermissionCategory(it.permissionCategory) == permissionType && 229 it.contributingPackages.isNotEmpty() && 230 it.contributingPackages 231 .map { contributingApp -> contributingApp.packageName } 232 .contains(packageName) 233 } != null 234 } 235 236 /** 237 * Represents Health Category group to be shown in health connect screens. 238 * 239 * @param category Category id 240 * @param data [HealthPermissionType]s within the category that have data written by given app. 241 */ 242 data class PermissionTypesPerCategory( 243 val category: @HealthDataCategoryInt Int, 244 val data: List<HealthPermissionType>, 245 ) 246